Whiteship's Note


스프링 이메일 확장하기

모하니?/Coding : 2009.07.29 21:49


Gmail의 SMTP를 이용하여 회원 가입시 메일을 보내서 회원 인증 링크를 보낸다. 그리고 해당 링크를 클릭할 시 회원 가입을 승인한다.

이러한 요구사항이 있습니다. 여기서 앞부분에 스프링 이메일을 사용하여 기능 구현을 하려고합니다.

먼저 GmailSender라는 녀석을 만들었습니다. 그러나 그런 클래스는 필요가 없었습니다. host, port, username, password만 설정하면 스프링이 제공하는 JavaMailSender가 GmailSender와 같은 녀석이 되기 때문이죠. 혹여나 그런 설정 이외에도 send()의 결과를 확인하고 싶어서 boolen 값을 반환해주도록 JavaMailSender의  send 메서드를 try-catch로 랩핑한 클래스를 만들었습니다. 하지만, 생각해보면 어차피 MailException은 런타임 예외기 때문에 false를 반환한다는 건 불가능한 일입니다. 오직 true 값을 반환하거나/MailExcpetion을 던지는 메서드가 되고 마는데... 이런 녀석이 존재할 필요가 있을까 싶어서 없애기로 결정했습니다.

없앨 코드 1.

    public boolean send(SpringSproutSimpleMailMessage mailMessage) {
        try {
            mailSender.send(mailMessage);
        } catch (MailException e) {
            throw e;
        }
        return true;
    }

또 하나 떠오르는 녀석은 SignupMailSender입니다. 이 Sender는 특정 Member를 인자로 받으면 해당 Member의 email로 Signup 관련 URL을 만들어서 보내주는 녀석입니다. 하지만 이 녀석도 잘 생각해보면, Sender의 책임이 두 가지가 되고 말았습니다. SRP를 위반하게 된거죠. 메시지를 만드는 일과 메일을 보내는 일이 합쳐졌습니다. 따라서 아래 코드의 대부분은  MailMessage 쪽으로 넘기는게 좋겠습니다.

리팩토링할 코드 2

    public boolean sendConfirmMail(Member receveMember) {
        List<Member> receives = new ArrayList<Member>();
        receives.add(receveMember);
        String authUrl;
        try {
            authUrl =
                    "http://localhost:8080/springsprout2/signupconfirm.do?email="+
                    URLEncoder.encode(receveMember.getEmail(), "UTF-8") +
                    "&authCode="+
                    URLEncoder.encode(receveMember.getAuthCode(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("can not encoding url", e);
        }

        SpringSproutSimpleMailMessage message = new SpringSproutSimpleMailMessage(
                "[봄싹 SpringSprout] 사용자 인증 메일",
                "사용자 계정을 활성화 하려면 다음 링크를 클릭하세요. <a href=\""+authUrl+"\">"+authUrl+"</a>",
                receives);
        return mailSendService.send(message);
    }

마지막으로 한 개의 클래스가 더 떠오르는데, 바로 위에 보이는 MailMessage를 확장한 S3M2(SpringSproutSimpleMailMessage) 클래스입니다. 하지만 이 클래스의 이름에서 클래스가 무슨 일을 하는지 의도를 파악하기가 힘듭니다. 이 클래스는 생성자로 subjer, text, member list를 받아서 from, subject, text, to 등의 속성을 세팅해주는 클래스 입니다. 아쉬운 건 아직 이 녀석 생성자에 단일 member 객체를 받는 생성자가 없다는 것이죠. 그래서 위의 코드를 보면 Member List를 만들어서 보내는 모습을 볼 수 있었죠.ㅎㅎ 이 MailMessage는 봄싹에서 사용할 기본 메일 메시지를 설정하기 때문에, Abstract 클래스로 만들고, 좀 더 구체적인 메시지로 가입승인메시지(SignupConfirmMailMessage) 같은 녀석이 그것을 확장하는 것이 좋아보입니다. 그러면 클래스의 이름에서 의도도 알 수 있고, S3M2에 있던 과중한 업무도 분담이 될테니 말이죠.

결국 아래와 같은 구조가 될 듯 합니다. 오늘 밤... 코드를 조금 뜯어 고쳐야겠습니다. 새기능 만들것도 많은데 이번 주는 뜯어 고치는데 시간을 다 쓰네요~ 유후~


ps: 참..오늘의 영어과제부터 하고나서
top


스프링의 이메일 기능 지원과 테스트를 살펴보자

Spring/etc : 2009.07.27 12:20


스프링이 지원하는 이메일은 JavaMail과 JAF라는 것이 있습니다. 사용법은 간단하니.. 다음에 심심할 때 살펴보기로 하고, 지금은 사부님이 올리신 글과 관련 된 부분을 찾아보는게 급선무입니다.

스프링 이메일 기능은 context.support 모듈에 들어있습니다. 주요 클래스는 o.s.mail.javamail에 들어있는 JavaMailSender 인터페이스와 그 구현체인 JavaMailSenderImpl 클래스입니다. 인터페이스의 API를 읽어보면 다음과 같은 내용이 있습니다.

...

Clients should talk to the mail sender through this interface if they need mail functionality beyond SimpleMailMessage. The production implementation is JavaMailSenderImpl; for testing, mocks can be created based on this interface. Clients will typically receive the JavaMailSender reference through dependency injection.

...

The entire JavaMail Session management is abstracted by the JavaMailSender. Client code should not deal with a Session in any way, rather leave the entire JavaMail configuration and resource handling to the JavaMailSender implementation. This also increases testability.

A JavaMailSender client is not as easy to test as a plain MailSender client, but still straightforward compared to traditional JavaMail code: Just let createMimeMessage() return a plain MimeMessage created with a Session.getInstance(new Properties()) call, and check the passed-in messages in your mock implementations of the various send methods.

즉, 이 API를 만든 의도에 사용성 편의 뿐만 아니라, 테스트 편의성도 포함되어 있다는 암시를 읽어낼 수 있습니다. JavaMail의 Session API를 사용하지 않고 스프링의 JavaMailSender를 목킹해서 테스트 하라는 것인데, 왜 그렇게 했는지는 맨 마지막 부분의 JavaMail의 Session API 사용법(Session.getInstance(new Properties())에서 볼 수 있습니다. 바로 static 메서드입니다.

JavaMail 레퍼런스에서 그 사용법을 보면, 다음과 같은 코드로 JavaMail을 이용하는 모습을 볼 수 있습니다.

//메시지를 만들고,,
     Properties props = new Properties();
     props.load(new FileInputStream(propfile));

     Session session = Session.getInstance(props, null);
     MimeMessage msg = new MimeMessage(session);

...

//전송합니다.
    // Set the content for the message and transmit
    msg.setContent(mp);
    Transport.send(msg);

코드 대부분을 생략했습니다. 중요한 부분은 위에 다 나와있습니다. 바로 static 메서드를 사용한다는 것이 중요한 부분입니다.

이런 API 사용을 클래스를 단위 테스트 하려면 막막합니다. 도무지 static 메서드를 호출하는 부분을 mock으로 바꿀 수가 없습니다. 그렇다고 테스트를 하는데 실제로 메일을 매번 보내기도 뭐하고 말이죠. 그래서 테스트 하려면 static 메서드 호출을 사용한 클래스를 거의 다시 만들다시피 구현한 stub을 만들어서 테스트 해야 하는데 이건 엄청난 수고가 필요합니다. JavaMail 예를 들면 거의 Transport를 테스트용으로 다시 구현해서.. sendMessage에서 실제로 메일을 보내지 않고 그냥 보내는 메시지 목록에 메시지만 모아두는 식의 작업이 필요해집니다. 그리고 테스트 할 떄는 그런 스텁 Transport를 사용하는 또 다른 스텁 MailSender가 필요해지겠죠.(실제로 이 작업들은 스프링의 JavaMailSenderTests에서 수행하고 있습니다.)

하지만, 테스트를 편하게 하는 방법이 아주 없는 건 아니었습니다. 오늘 톱님께서 올리신 글을 보면 static 메서드를 호출하는 코드를 비교적 편하게 테스트하는 방법 세 가지를 알 수 있습니다.

하나는 JavaMailSender처럼 static 메서드 호출 부분을 랩핑한 클래스를 만들고, 그 클래스를 목킹한 다음 해당 메서드가 호출되는지 테스트하는 것입니다. 그렇게 만들어 두면, 테스트 하려는 대상이 JavaMail의 Transport.send 같은 static 메서드를 호출하지 않고, 그것을 사용한 JavaMailSender를 사용하기 때문에 JavaMailSender의 mock을 만들고 그 객체의 send가 호출될 때 어떤걸 하라고 mocking 한다던지 해서 호출이 제대로 됐는지, 어떤 메시지가 넘어갔는지 등을 확인할 수 있겠습니다.

두 번째 방법은 로드존슨이 만들었다는 AspectJ를 이용하는 방법이고, 세 번째 방법은 PowerMock을 이용하는 방법인데, 둘 다 결국 바이트코드를 조작해서 static 호출 부분을 mock으로 호출로 교체하는 기술 인듯합니다. 이 두 가지는 일단 논외로 치

결론은.. 스프링의 JavaMailSender를 사용하면 JavaMail의 static 메서드 호출과 관련하여 테스트를 어떻게 작성할까 고민할 필요없이, JavaMailSender를 목킹해서 테스트를 만들면 된다는 것입니다.

덤으로 JavaMail의 static 메서드 호출을 사용한 JavaMailSender의 테스트 클래스를 보면, static 메서드 호출을 하는 클래스에 대한 테스트 실마리를 얻을 수 있습니다. static 메서드를 호출해서 가져오는 객체를 가져오는 부분을 별도의 메서드로 분리하고 그 부분을 테스트 용으로 재구현하고 그것을 테스트에서 사용하는 방법인데.. 이거 이거.. 귀찮아서 원... @_@..  그래서 static은 테스트의 적인가 봅니다. 그래도 이길 수 있는 적이라는거..
top