3 분 소요

1. Google 메일 설정

spring boot를 이용하여 메일을 발송할 수 있습니다. 이번 글에서는 구글 메일을 통해 발송할것 입니다. 구글 메일을 이용하기 위해서는 다음과 같은 설정이 필요합니다.

  1. Google 홈페이지 > Google 계정 관리mail1
  2. 보안 > 앱 비밀번호(이전에 2단계 인증설정이 필요)mail2
  3. 메일, Windows 컴퓨터mail3
  4. 앱 비밀번호 저장(smtp 설정에 사용될 예정)mail4
  5. Gmail > 빠른 설정(우측상단 톱니바퀴) > 모든 설정 보기mail5
  6. 전달 및 POP/IMAP(탭) > POP다운로드: 모든 메일에 POP사용하기 > IMAP 액세스: IMAP사용 > 변경사항 저장mail6

2. Spring boot 에서 설정

gradle 에 추가

implementation 'org.springframework.boot:spring-boot-starter-mail'

SMTP 설정

application-mail.yml

아래와 같이 username, password 부분을 환경변수 처리하여 이용할 수 있습니다.

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: ${MAIL_USERNAME} #SMTP용 google 계정(gmail 아이디)
    password: ${MAIL_PASSWORD} # 앱 비밀번호
    properties:
      mail:
        smtp:
          starttls:
            enable: true
            required: true
          auth: true
          connectiontimeout: 5000
          timeout: 5000
          writetimeout: 5000

3. mail 발송

메일을 보내기 위해서는 수신자의 메일주소와 메일 제목, 내용이 필요합니다. 아래 코드에서 NotificationEmail 에 해당 내용들을 채워줍니다.

아래 로직은 controller 에서 수신자의 메일주소를 받고 AuthService에서 만들어진 NotificationEmail 을 바탕으로 MailService에서 실제로 메일을 보냅니다. 타임리프를 이용하여 미리 내용을 채워야 하는 메일 템플릿을 만들어 두고 해당 템플릿 파일의 이름채워야 하는 내용의 이름, 값으로 된 Map 을 통해 MailContentBuilder 에서 내용을 채웁니다. 여기서 주의할 점은 테스트 코드에서 mock처리를 고려하여 TemplateEngine 이 아닌 ITemplateEngine을 이용해야 합니다. (TemplateEngine 이용시 NPE 발생)

이후 MimeMessageHelper 를 통해 내용을 채웁니다. 이미지/업로드 전송을 위해서는 멀티파트 메시지 허용, html 허용 설정을 해줘야 합니다. 최종적으로 JavaMailSender의 send() 메소드로 메일을 전송합니다.

메일 전송을 할때는 비동기화 처리(@Async)를 하여 메일 전송이 끝날때까지 기다리지 않도록 합니다.

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class NotificationEmail {
	private String receiver;
	private String subject;
	private String content;

	public static NotificationEmail of(String receiver, String subject, String content) {
		return new NotificationEmail(receiver, subject, content);
	}
}


public class AuthService {
    private final MailService mailService;
	private final MailContentBuilder mailContentBuilder;

	public String sendAuthenticationCodeEmail(final EmailRequest emailRequest) {
		String code = AuthenticationCode.createCode().getCode();
		String message = createAuthenticationCodeEmailContent(code);
		NotificationEmail notificationEmail =
			NotificationEmail.of(emailRequest.getEmail(), AUTH_CODE_MAIL_TITLE, message);
		log.info("AuthService Thread: " + Thread.currentThread().getName());
		mailService.sendMail(notificationEmail);

		return code;
	}

	private String createAuthenticationCodeEmailContent(final String code) {
		Map<String, String> contents = new HashMap<>();
		contents.put(CODE_KEY, code);
		return mailContentBuilder.build(AUTH_CODE_TEMPLATE, contents);
	}
}

public class MailContentBuilder {
	// TemplateEngine을 이용할 경우 @Mock 사용시 NPE 발생
	private final ITemplateEngine templateEngine;

	public String build(final String template, final Map<String, String> contents) {
		Context context = new Context();

		for (String key : contents.keySet()) {
			context.setVariable(key, contents.get(key));
		}

		return templateEngine.process(template, context);
	}
}

public class MailService {
	private static final String ENCODING = "UTF-8";

	private final JavaMailSender mailSender;

	@Async
	public void sendMail(final NotificationEmail notificationEmail) {
		MimeMessagePreparator messagePreparator = toMimeMessagePreparator(notificationEmail);

		try {
			log.info("MailService Thread: " + Thread.currentThread().getName());
			mailSender.send(messagePreparator);
		} catch (MailException e) {
			throw new CMailException();
		}
	}

	private MimeMessagePreparator toMimeMessagePreparator(NotificationEmail notificationEmail) {
		MimeMessagePreparator messagePreparator = mimeMessage -> {
            // 이미지/업로드 전송을 위해서는 멀티파트 메시지 허용, html 허용 설정
            // true는 멀티파트 메시지를 사용하겠다는 의미
			MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, ENCODING);

			messageHelper.setTo(notificationEmail.getReceiver());
			messageHelper.setSubject(notificationEmail.getSubject());
            
            // true는 html 을 사용한다는 의미
			messageHelper.setText(notificationEmail.getContent(), true);
		};

		return messagePreparator;
	}
}
  • MailSender
    • 간단한 전자 메일을 보낼 수 있는 기능을 제공하는 최상위 인터페이스
  • JavaMailSender
    • MailSender를 상속받는 하위 인터페이스로 MIME 메시지를 지원하며 대부분 MimeMessage와 함께 사용합니다.
  • JavaMailSenderImpl
    • JavaMailSender 인터페이스를 구현한 클래스로 MimeMessage 및 SimpleMailMessage를 지원합니다.
  • SimpleMailMessage
    • 발신인, 수신인, 참조인, 제목 및 텍스트 필드를 포함한 간단한 메일 메시지를 작성하는데 사용됩니다.
  • MimeMessagePreparator
    • MIME 메시지를 준비하기 위한 콜백 인터페이스 제공합니다.
  • MimeMessageHelper
    • MIME 메시지를 만들기 위한 클래스로 HTML 레이아웃에서 이미지, 일반적인 메일 첨부 파일 및 텍스트 내용을 지원합니다.

**<참고>**

MIME는 Multipurpose Internet Mail Extensions의 약자입니다. 기존 UUEncoding 방식은 ASCII(텍스트) 파일만 지원하여 음악, 워드 파일 등의 바이너리 파일을 전송할 수 없습니다. 이러한 방식을 보안하여 나온 인코딩 방식입니다.

<!DOCTYPE HTML>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>authentication code</title>
</head>
<body>
<div style='margin:100px;'>
    <h1> 안녕하세요</h1>
    <br>
    <p>아래 코드를 회원가입 창으로 돌아가 입력해주세요</p>
    <br>
    <div align='center' style='border:1px solid black; font-family:verdana' ;>
        <h3 style='color:blue;'>회원가입 인증 코드입니다.</h3>
        <div style='font-size:130%'>
            CODE : <strong><span th:text="${code}">인증코드</span></strong>
        </div>
        <br>
    </div>

</div>
</body>
</html>

th:text=”${code} 의 code 에 내용이 채워지는 것으로 앞서 context.setVariable(key, contents.get(key)); 을 통해 내용을 채운것을 확인할 수 있습니다. (key: code, 값: contents.get(key))

무료 메일 템플릿

reference

참고1

댓글남기기