Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 25장 이메일

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



24. 이메일


25.1 소개

스프링 프레임워크는 유용한 유틸리티 라이브러리를 제공하는데 이 라이브러리는 기반 메일링 시스템의 특화된 부분에서 사용자를 보호하면서 이메일을 발송하고 클라이언트를 대신해서 저수준 리소스 처리를 담당한다.

org.springframework.mail 패키지가 스프링 프레임워크 이메일 지원의 최상위 패키지다. 이메일을 발송하는 중심 인터페이스는 MailSender 인터페이스다. from, to같은 메일 속성들을 은닉화한 간단한 값 객체(value object)는 SimpleMailMessage 클래스이다. 이 패키지는 최상위 예외인 MailException를 던지는 저수준 메일 시스템 예외를 고수준으로 추상화한 체크드 익셉션 계층도 포함되어 있다. 리치 메일 예외 계층에 대한 자세한 내용은 JavaDoc을 참고해라.

org.springframework.mail.javamail.JavaMailSender 인터페이스는 MIME 메시지처럼 특수한 JavaMail 기능을 MailSender 인터페이스(혹은 이 인터페이스를 상속받은)에 추가한다. MailSender도 JavaMail MIME 메시지를 준비하기 위한 콜백 인터페이스 org.springframework.mail.javamail.MimeMessagePreparator를 제공한다.

라이브러리 의존성 스프링 프레임워크의 이메일 라이브러리를 사용하려면 애플리케이션의 클래스패스에 다음의 jar를 추가해야 한다. * JavaMail mail.jar 라이브러리 * JAF activation.jar 라이브러리 이 라이브러리들은 인터넷에서 무료로 구할 수 있다.


25.2 사용방법

OrderManager라는 비즈니스 인터페이스가 있다고 가정해 보자.

public interface OrderManager {
  void placeOrder(Order order);
}

순서를 가진 이메일 메시지를 생성해야 하고 이 순서에 따라 고객에게 이메일을 발송해야 하는 요구사항이 있다고 해보자.

25.2.1 MailSender와 SimpleMailMessage의 기본 사용방법

import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

public class SimpleOrderManager implements OrderManager {
  private MailSender mailSender;
  private SimpleMailMessage templateMessage;

  public void setMailSender(MailSender mailSender) {
    this.mailSender = mailSender;
  }

  public void setTemplateMessage(SimpleMailMessage templateMessage) {
    this.templateMessage = templateMessage;
  }

  public void placeOrder(Order order) {
    // 비즈니스 연산을 수행...

    // 순서를 유지할 콜라보레이터를 호출...

    // 템플릿 메시지를 스레드 세이프하게 "복사본"을 생성하고 커스터마이징한다
    SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage);
    msg.setTo(order.getCustomer().getEmailAddress());
    msg.setText(
      "Dear " + order.getCustomer().getFirstName()
        + order.getCustomer().getLastName()
        + ", thank you for placing order. Your order number is "
        + order.getOrderNumber());
    try{
      this.mailSender.send(msg);
    }
    catch(MailException ex) {
      // 로깅 등...
      System.err.println(ex.getMessage());
    }
  }
}

위 코드의 빈 정의가 다음에 나와 있다.

<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
  <property name="host" value="mail.mycompany.com"/>
</bean>


<bean id="templateMessage" class="org.springframework.mail.SimpleMailMessage">
  <property name="from" value="customerservice@mycompany.com"/>
  <property name="subject" value="Your order"/>
</bean>

<bean id="orderManager" class="com.mycompany.businessapp.support.SimpleOrderManager">
  <property name="mailSender" ref="mailSender"/>
  <property name="templateMessage" ref="templateMessage"/>
</bean>


25.2.2 JavaMailSender와 MimeMessagePreparator 사용하기

다음은 MimeMessagePreparator 콜백 인터페이스를 사용하는 OrderManager의 또 다른 구현체다. 이 경우 mailSender 프로퍼티가 JavaMailSender 타입이므로 JavaMail MimeMessage 클래스를 사용할 수 있다는 점을 주의 깊게 봐라.

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import javax.mail.internet.MimeMessage;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessagePreparator;

public class SimpleOrderManager implements OrderManager {
  private JavaMailSender mailSender;

  public void setMailSender(JavaMailSender mailSender) {
    this.mailSender = mailSender;
  }

  public void placeOrder(final Order order) {
    // 비즈니스 연산을 수행...

    // 순서를 유지할 콜라보레이터를 호출...

    MimeMessagePreparator preparator = new MimeMessagePreparator() {

      public void prepare(MimeMessage mimeMessage) throws Exception {
        mimeMessage.setRecipient(Message.RecipientType.TO,
            new InternetAddress(order.getCustomer().getEmailAddress()));
        mimeMessage.setFrom(new InternetAddress("mail@mycompany.com"));
        mimeMessage.setText(
          "Dear " + order.getCustomer().getFirstName() + " "
            + order.getCustomer().getLastName()
            + ", thank you for placing order. Your order number is "
            + order.getOrderNumber());
      }
    };
    try {
      this.mailSender.send(preparator);
    }
    catch (MailException ex) {
      // 로깅 등...
      System.err.println(ex.getMessage());
    }
  }
}
Note
메일 코드는 크로스컷팅 관심(crosscutting concern)이고 custom Spring AOP aspect로 리팩토링할 대상이 될 수 있으며 이는 OrderManager를 대상으로 한 적절한 조인포인트에서 실행될 수 있다.

스프링 프레임워크의 메일 지원은 JavaMail 구현체와 함께 제공된다. 더 자세한 내용은 JavaDoc을 참고해라.

25.3 JavaMail MimeMessageHelper 사용하기

JavaMail 메시지를 상당히 편하게 다룰 수 있게 하는 클래스는 org.springframework.mail.javamail.MimeMessageHelper로 복잡한 JavaMail API를 편하게 사용할 수 있다. MimeMessageHelper를 사용하면 MimeMessage를 아주 쉽게 생성할 수 있다.

// 당연히 실제 프로젝트에서는 DI를 사용할 것이다
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");

MimeMessage message = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setTo("test@host.com");
helper.setText("Thank you for ordering!");

sender.send(message);


25.3.1 첨부 파일이나 리소스를 내장해서 발송하기

첨부파일과 내장 리소스에 모두 멀티파트 이메일 메시지를 사용할 수 있다. 내장 리소스의 예시로는 메시지에서 사용할 이미지나 스타일 시트 등인데 대신 첨부 파일로 보이기는 원치 않는 리소스들이다.

25.3.1.1 첨부파일

다음 예제는 JPEG 이미지 첨부 파일이 있는 이메일을 보내는 MimeMessageHelper의 사용방법을 보여준다.

JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");

MimeMessage message = sender.createMimeMessage();

// 멀티파트 메시지가 필요하다는 의미로 true 플래그를 사용한다
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("test@host.com");

helper.setText("Check out this image!");

// 악명높은 윈도우의 Sample 파일을 첨부하자 (여기서는 c:/ 에서 복사한다)
FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addAttachment("CoolImage.jpg", file);

sender.send(message);


25.3.1.2 내장 리소스

다음 예제는 내장 이미지를 가진 이메일을 보내는 MimeMessageHelper의 사용방법을 보여준다.

JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");

MimeMessage message = sender.createMimeMessage();

// 멀티파트 메시지가 필요하다는 의미로 true 플래그를 사용한다
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("test@host.com");

// 포함된 텍스트가 HTML이라는 의미로 true 플래그를 사용한다
helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true);

// 악명높은 윈도우의 Sample 파일을 첨부하자 (여기서는 c:/ 에서 복사한다)
FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addInline("identifier1234", res);

sender.send(message);
Warning
특정 Content-ID로(이 예제에서는 identifier1234) mime 메시지에 내장 리소스를 추가한다. 추가하는 텍스트와 리소스의 순서는 아주 중요하다. 텍스트를 먼저 추가하고 리소스를 나중에 추가해라. 반대로 한다면 동작하지 않는다!


25.3.2 템플릿 라이브러리로 이메일 콘텐츠를 생성하기

앞의 예제에서는 이메일 메시지의 내용을 message.setText(..)같은 메서드를 호출해서 명시적으로 생성했다. 앞의 예제처럼 간단한 이메일이라면 괜찮다.(앞의 예제는 기본적인 API를 보여주기 위함이었다.)

하지만 전형적인 엔터프라이즈 애플리케이션이라면 여러 가지 이유로 위의 방법으로 이메일의 내용을 만들지 않을 것이다.

  • Java 코드로 HTML 기반의 이메일 내용을 생성하는 것은 지루하고 오류가 발생할 가능성도 크다
  • 표현 로직과 비즈니스 로직 간의 구분이 쉽지 않다
  • 이메일 내용의 표현 구조를 변경하려면 자바 코드를 수정하고 다시 컴파일한 다음에 배포해야 한다

이 문제를 해결하는 일반적인 방법은 이메일 콘텐츠의 표현 구조를 정의할 때 FreeMarker나 Velocity 같은 템플릿 라이브러리를 사용하는 것이다. 템플릿 라이브러리를 사용하면 코드는 데이터를 생성하는 작업만 하고 이 데이터를 이메일 템플릿으로 렌더링해서 이메일을 발송한다. 이메일의 내용이 꽤 복잡하더라도 템플릿을 사용하는 것이 아주 좋은 방법이고 스프링 프레임워크의 FreeMarker와 Velocity 지원 클래스로 아주 쉽게 사용할 수 있다. Velocity 템플릿 라이브러리로 이메일 내용을 생성하는 예제가 다음에 나와 있다.

25.3.2.1 Velocity 기반 예제

이메일 템플릿을 만들 때 Velocity를 사용하려면 Velocity 라이브러리를 클래스패스에 두고 어플리케이션에서 사용할 이메일 컨텐츠의 Velocity 템플릿을 하나이상 만들어야 한다. 이 예제에서 사용할 Velocity 템플릿이 다음에 나와 있다. 예제에서 보듯이 템플릿은 HTML 기반이고 일반적인 텍스트이므로 자신이 선호하는 HTML 에디터나 텍스트 에디터로 템플릿을 작성할 수 있다.

# com/foo/package에 있다
<html>
<body>


Hi ${user.userName}, welcome to the Chipping Sodbury On-the-Hill message boards!

Your email address is ${user.emailAddress}.
</body> </html>

위의 Velocity 템플릿을 사용해서 이메일을 작성하고 보내는 간단한 코드와 스프링 XML 설정이 다음에 나와 있다.

package com.foo;

import org.apache.velocity.app.VelocityEngine;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.ui.velocity.VelocityEngineUtils;

import javax.mail.internet.MimeMessage;
import java.util.HashMap;
import java.util.Map;

public class SimpleRegistrationService implements RegistrationService {
  private JavaMailSender mailSender;
  private VelocityEngine velocityEngine;

  public void setMailSender(JavaMailSender mailSender) {
   this.mailSender = mailSender;
  }

  public void setVelocityEngine(VelocityEngine velocityEngine) {
   this.velocityEngine = velocityEngine;
  }

  public void register(User user) {

    // 등록과 관련된 로직...

    sendConfirmationEmail(user);
  }

  private void sendConfirmationEmail(final User user) {
    MimeMessagePreparator preparator = new MimeMessagePreparator() {
      public void prepare(MimeMessage mimeMessage) throws Exception {
        MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
        message.setTo(user.getEmailAddress());
        message.setFrom("webmaster@csonth.gov.uk"); // 파라미터화할 수 있다...
        Map model = new HashMap();
        model.put("user", user);
        String text = VelocityEngineUtils.mergeTemplateIntoString(
           velocityEngine, "com/dns/registration-confirmation.vm", model);
        message.setText(text, true);
      }
    };
    this.mailSender.send(preparator);
  }
}

<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
  <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="host" value="mail.csonth.gov.uk"/>
  </bean>

  <bean id="registrationService" class="com.foo.SimpleRegistrationService">
    <property name="mailSender" ref="mailSender"/>
    <property name="velocityEngine" ref="velocityEngine"/>
  </bean>

  <bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
    <property name="velocityProperties">
       <value>
        resource.loader=class
        class.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
       </value>
    </property>
  </bean>
</beans>
2014/04/30 23:48 2014/04/30 23:48