최근 구독 만료 일자가 얼마 남지 않은 사용자를 대상으로 구독기간 만료 예정 안내 메일을 보내야 했습니다.
한국에는 관련 법률이 있는지 잘 모르지만, 미국 특정 주에서는 관련 법률이 있다는 것을 확인했습니다.
메일 서버는 AWS에서 제공하는 SES (Simple Email Service)를 사용하였습니다.
AWS에서 제공하는 API를 사용하거나, SMTP 인터페이스를 사용하는 두 가지 방법 중
추후 호환성을 위해 SMTP 인터페이스를 구현하였습니다.
SES SMTP 자격 증명을 얻는 것은 아래의 사이트를 참고해주시길 바랍니다.
SmtpMailSender.java
public void send(String to, String subject, String content) {
Properties properties = System.getProperties();
properties.put("mail.transport.protocol", "smtp");
properties.put("mail.smtp.port", port);
properties.put("mail.smtp.starttls.enable", "true");
properties.put("mail.smtp.auth", "true");
Session session = Session.getDefaultInstance(properties);
MimeMessage mimeMessage;
try {
Address address = new InternetAddress(to);
mimeMessage = new MimeMessage(session);
mimeMessage.setFrom(new InternetAddress(fromEmail, fromName));
mimeMessage.setRecipient(Message.RecipientType.TO, address);
mimeMessage.setSubject(subject, StandardCharsets.UTF_8.name());
mimeMessage.setContent(content, "text/html;charset=UTF-8");
} catch (MessagingException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
try (Transport transport = session.getTransport()) {
log.info("Sending...");
transport.connect(host, username, password);
transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients());
log.info("Email Sent!");
} catch (MessagingException e) {
log.error("The email was not sent.");
log.error("Error message: " + e.getMessage());
}
}
SMTP 인터페이스를 통해 메일을 보내는 코드입니다.
추후 메일 서버가 바뀌더라도 코드의 변경 없이 메일을 보낼 수 있습니다.
application.yml
aws:
ses:
from: 송신자 이메일
from-name: 송신자 이름
smtp:
host: email-smtp.ap-northeast-2.amazonaws.com
port: 587
username: AWS SES SMTP USERNAME
password: AWS SES SMTP PASSWORD
송신에 필요한 데이터는 application.yml 에 넣어 코드와 분리를 시켰습니다.
메일을 보내기 위한 준비는 끝났으니, 하나의 예시를 들어보겠습니다.
저희가 메일을 보낼 대상은 "곧 구독이 만료되는 사용자"입니다.
월간 구독, 연간 구독이 있다고 가정하면
월간 구독은 만료 10일 전, 연간 구독은 만료 25일 전에 메일을 보내야 합니다.
메일은 다음과 같이 보내려고 합니다.
<h1>월간 구독 만료 안내입니다.</h1>
<p>안녕하세요? 회원님의 구독이 2022-11-10에 만료가 됩니다. 감사합니다.</p>
반드시 변경이 되어야 하는 값은 날짜입니다.
추가로, 위의 제목도 구독 유형에 따라 변경이 될 수 있습니다.
즉, HTML 파일은 하나로만 관리하고, 구독 유형에 따라 제목과 날짜를 바꾼다고 하면
다음과 같이 이메일 템플릿을 작성할 수 있습니다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${title}"></title>
</head>
<body>
<h1 th:text="${title}"></h1>
<p>안녕하세요? 회원님의 구독이 <span th:text="${expireDate}"></span>에 만료가 됩니다. 감사합니다.</p>
</body>
</html>
Thymeleaf를 사용하여 이메일을 보내는 방법은 위 공식 블로그를 참고하였습니다.
TemplateEngine을 사용해 이메일 템플릿을 해석하여 변환합니다.
어떤 내용이 들어갈지는 천차만별이지만, 메일을 보내는 과정은 크게 다르지 않을 것입니다.
MailSenderController.java
@PostMapping
public ResponseEntity<Void> sendSubscriptionExpireMail(@Validated @RequestBody MailSenderRequest.Send request) {
int threads = Runtime.getRuntime().availableProcessors() + 1;
ExecutorService executorService = Executors.newFixedThreadPool(threads);
request.getTo().forEach(to -> {
executorService.submit(() -> {
Context context = getSubscriptionExpireMailContext(request.getSubscriptionType());
String html = templateEngine.process("subscription-expire", context);
mailService.send(to, (String) context.getVariable("title"), html);
});
});
executorService.shutdown();
return null;
}
private Context getSubscriptionExpireMailContext(SubscriptionType subscriptionType) {
Context context = new Context();
int daysToAdd;
String title;
switch (subscriptionType) {
case MONTHLY:
daysToAdd = 10;
title = "월간 구독 만료 안내입니다.";
break;
case ANNUALLY:
daysToAdd = 25;
title = "연간 구독 만료 안내입니다.";
break;
default:
throw new IllegalStateException("처리할 수 없는 메일 타입입니다.");
}
context.setVariable("expireDate", LocalDate.now().plusDays(daysToAdd));
context.setVariable("title", title);
return context;
}
대상자가 많을 경우를 대비해 멀티 스레드로 작동되도록 했습니다.
스레드는 ExecutorService를 사용하여 스레드 풀 관리를 하였습니다.
Spring Web 의존성을 추가했기 때문에, POST 요청을 보내면 메일을 보낼 수 있습니다.
POST http://localhost:8080/mail
Content-Type: application/json
{
"to": [
"test@test.com",
"test2@test.com"
],
"subscriptionType": "MONTHLY" // ANNUALLY는 연간
}
위 포스트에 적힌 코드는 아래의 Repo에서 확인할 수 있습니다.
'Java > Spring' 카테고리의 다른 글
@RequestBody에 필드가 하나밖에 없을 때 매핑이 되지 않는 이유 (2) | 2024.10.13 |
---|---|
messages.properties 숫자 대신 문자 인덱스 사용하기 (0) | 2022.06.27 |