Spring Boot 이메일 발송 설정: JavaMailSender 사용법
Spring Boot로 넘어오면서 메일 발송 설정이 XML 파일에서 application.properties 몇 줄로 줄었습니다. 예전에는 메일 설정 파일을 따로 만들어 Spring 설정에 물려줘야 했지만, 지금은 의존성 하나와 프로퍼티 몇 개면 JavaMailSender를 주입받아 바로 씁니다. 아래는 Gmail SMTP를 기준으로 한 최소 구성이고, 각 설정값이 실제로 무엇을 켜는지는 공식 문서에서 확인한 내용을 함께 적었습니다. 발송 코드 자체는 짧지만 실패했을 때 무엇이 던져지는지까지 알아두면 나중에 덜 헤맵니다.
Boot가 JavaMailSender를 만들어 주는 조건
먼저 pom.xml에 의존성을 추가합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
Spring Boot 레퍼런스의 Sending Email 문서에는 기본 JavaMailSender가 만들어지는 조건이 그대로 적혀 있습니다. spring.mail.host가 설정되어 있을 것, spring-boot-starter-mail이 정의하는 라이브러리가 사용 가능할 것, 그리고 이미 등록된 JavaMailSender 빈이 없을 것. 세 가지가 맞으면 설정 클래스를 따로 쓰지 않아도 빈이 올라옵니다. 반대로 의존성만 넣고 host를 비워 두면 이 조건이 충족되지 않습니다.
빈을 직접 조립하는 길도 남아 있습니다. Spring Framework 레퍼런스의 Email 문서에는 JavaMailSenderImpl을 @Bean으로 만들고 setHost로 서버를 지정하는 예제가 나옵니다. Boot를 쓰는 동안 그럴 일은 거의 없지만, 자동 구성이 대신 해주는 일이 결국 이 코드라는 점은 알아두면 좋습니다. XML 시절에 손으로 채우던 항목들이 프로퍼티 키로 옮겨간 것뿐입니다.
application.properties에 넣는 값
spring.mail.host=smtp.gmail.com
spring.mail.port=465
spring.mail.username=xxxxx@gmail.com
spring.mail.password=xxxxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.ssl.enable=true
앞의 네 줄은 접속 정보입니다. 뒤의 두 줄, spring.mail.properties로 시작하는 값은 Spring Boot 공통 애플리케이션 프로퍼티 문서에 "Additional JavaMail session properties"로 설명되어 있습니다. JavaMail 세션에 그대로 넘어가는 값이라는 뜻입니다. 이 설정은 옵션입니다. 메일 서버에 따라 해줘야 할 때도 있고, 안 해도 되는 경우도 있습니다.
두 값의 의미는 Jakarta Mail의 SMTP 프로토콜 프로바이더 문서에 있습니다. mail.smtp.auth는 true일 때 AUTH 명령으로 사용자 인증을 시도하며 기본값은 false입니다. mail.smtp.ssl.enable은 true면 SSL로 접속하고 SSL 포트를 기본 포트로 사용하는데, smtp 프로토콜에서는 기본 false, smtps 프로토콜에서는 기본 true입니다. 같은 문서 기준으로 mail.smtp.port의 기본값은 25이므로, 다른 포트를 쓰려면 spring.mail.port로 직접 적어야 합니다.
Boot 쪽 기본값도 같이 보면 설정이 짧은 이유가 보입니다. 공통 프로퍼티 문서에 spring.mail.protocol은 smtp, spring.mail.default-encoding은 UTF-8, spring.mail.test-connection은 false로 적혀 있습니다. 프로토콜과 인코딩을 안 적은 위 설정은 그 기본값을 그대로 씁니다. 적을 값이 적다는 건 반대로 말하면 문서에서 기본값을 한 번은 확인해야 한다는 뜻이기도 합니다.
서비스에 주입해서 보내기
설정이 끝나면 바로 쓸 수 있습니다. 저는 서비스를 따로 만들어 씁니다.
@Service("emailSendService")
public class EmailSendServiceImpl implements EmailSendService {
@Autowired
private JavaMailSender mailSender;
@Override
public void send(final String fromMail, final String toMail, final String mailTitle,
final String message) {
SimpleMailMessage email = new SimpleMailMessage();
email.setFrom(fromMail);
email.setTo(toMail);
email.setSubject(mailTitle);
email.setText(message);
mailSender.send(email);
}
}
Spring Framework 레퍼런스를 보면 org.springframework.mail이 메일 지원의 루트 패키지이고, 발송의 중심 인터페이스는 MailSender입니다. SimpleMailMessage는 from, to 같은 값을 담는 단순 값 객체로 설명됩니다. JavaMailSender는 그 MailSender를 상속하면서 MIME 메시지 지원 같은 JavaMail 고유 기능을 얹은 인터페이스입니다. 상속 관계가 이렇기 때문에 JavaMailSender를 주입받은 위 코드에서도 SimpleMailMessage를 그대로 send에 넘길 수 있습니다.
구현체인 JavaMailSenderImpl javadoc에도 MimeMessage와 SimpleMailMessage를 모두 지원하는 구현이라고 적혀 있습니다. 이 클래스의 DEFAULT_PROTOCOL은 smtp, DEFAULT_PORT는 -1입니다. 레퍼런스 예제는 한 걸음 더 나가서, 미리 만들어 둔 SimpleMailMessage를 복사 생성자로 받아 수신자와 본문만 갈아 끼웁니다. 발신자와 제목이 매번 같은 알림 메일이라면 그 방식이 손이 덜 갑니다.
실패하면 무엇이 던져지나
위 코드에는 예외 처리가 없습니다. 발송이 실패하면 호출한 쪽으로 그대로 올라갑니다. Spring의 메일 예외는 MailException을 뿌리로 하는 계층이고, MailException javadoc을 보면 NestedRuntimeException을 상속합니다. RuntimeException 계열이라 잡지 않아도 컴파일은 통과합니다. 직접 하위 클래스는 넷입니다.
- MailAuthenticationException: 인증 관련 오류
- MailParseException: 메시지 파싱 오류
- MailPreparationException: 메시지 준비 단계 오류
- MailSendException: 발송 단계 오류
계정 정보가 틀린 경우와 서버까지 갔다가 거절당한 경우가 서로 다른 타입으로 갈립니다. 로그에 찍힌 예외 이름만 봐도 어느 쪽을 손봐야 하는지 갈라집니다. NestedRuntimeException을 상속한 덕분에 getRootCause나 getMostSpecificCause로 원인 예외를 바로 꺼낼 수도 있습니다. 레퍼런스의 주문 처리 예제는 send를 try로 감싸고 MailException을 잡아 메시지를 출력하는 형태를 보여줍니다. 메일 실패가 업무 트랜잭션 전체를 되돌릴 만한 일인지는 서비스마다 다르니, 잡을지 흘려보낼지는 거기에 맞춰 정하면 됩니다.
타임아웃, 첨부파일, JNDI
Boot 레퍼런스가 메일 항목에서 따로 지면을 할애하는 것이 타임아웃입니다. 기본값이 무한이라 메일 서버가 응답하지 않으면 스레드가 묶일 수 있다는 설명과 함께 세 개의 프로퍼티를 예로 듭니다.
spring.mail.properties[mail.smtp.connectiontimeout]=5000
spring.mail.properties[mail.smtp.timeout]=3000
spring.mail.properties[mail.smtp.writetimeout]=5000
지금 레퍼런스는 이렇게 대괄호 표기를 씁니다. Jakarta Mail SMTP 문서에서도 connectiontimeout은 소켓 연결 타임아웃, timeout은 소켓 읽기 타임아웃이며 둘 다 기본이 무한이라고 적고 있습니다. 로컬에서는 티가 안 나다가 운영에서 문제가 되는 값이라, 배포 전에 한 번은 채워 넣는 편이 낫습니다.
첨부파일이나 HTML 본문은 SimpleMailMessage로는 처리되지 않습니다. 레퍼런스는 JavaMailSender의 createMimeMessage로 MimeMessage를 만들고 MimeMessageHelper로 감싸는 방식을 예제로 보여줍니다. 헬퍼 생성자의 두 번째 인자를 true로 주면 multipart가 되고, addAttachment로 파일을 붙이거나 addInline으로 본문 안에 이미지를 심습니다. setText의 두 번째 인자를 true로 주면 HTML 본문이 됩니다. 같은 문서에는 본문이 복잡해지면 자바 코드에서 HTML을 조립하지 말고 템플릿 라이브러리를 쓰라는 권고도 붙어 있습니다.
메일에 붙일 PDF를 서버에서 만들어야 한다면 Spring 4에서 JasperReports로 PDF를 출력한 글을 같이 보면 됩니다. 컨테이너가 관리하는 메일 세션을 쓰는 환경이라면 spring.mail.jndi-name을 지정하는 방법도 있습니다. Boot 문서는 이 값이 설정되면 다른 Session 관련 설정보다 우선한다고 못 박아 두었습니다. 호스트와 포트를 아무리 고쳐도 반영이 안 된다면 이 프로퍼티부터 확인해 볼 자리입니다.
라이브러리 붙이는 설정 정리는 안드로이드 쪽에도 따로 남겨 뒀습니다. 뷰 바인딩은 Butter Knife 설정에, JSON 변환은 Gson으로 자바 객체와 JSON을 다루는 방법에 적어 두었습니다.
정리
- spring.mail.host 설정 + starter-mail 라이브러리 + 기존 빈 없음, 이 세 조건에서 기본 JavaMailSender 자동 등록
- spring.mail.properties 하위 값은 JavaMail 세션으로 그대로 전달, mail.smtp.auth와 mail.smtp.ssl.enable은 smtp 기준 기본값 false
- 단순 텍스트는 SimpleMailMessage, 첨부파일과 HTML 본문은 MimeMessageHelper
- 실패는 MailException 계층으로 전달되며 인증 실패와 발송 실패가 별도 타입
- 배포 전 점검 항목으로 타임아웃 세 개, 문서상 기본값은 무한
출처: Sending Email :: Spring Boot, Email :: Spring Framework, JavaMailSenderImpl javadoc, MailException javadoc, com.sun.mail.smtp 패키지 문서, Spring Boot 공통 애플리케이션 프로퍼티