SMTP
콘솔로 찍던 javaMailSendor 대신 구글의 SMTP를 이용해 실제 메일을 보낸다.
먼저 구글 계정이 필요하다.
가입 완료했으면 계정관리 > 보안에 들어가 2단계 인증을 한다.
이후 2단계 인증 탭을 클릭하고 스크롤을 아래로 내리면 앱 비밀번호를 만들 수 있는 탭이 있다.
2021년부터 앱 비밀번호는 구글에서 권장하지 않는 방법이고 실제 이메일 서비스보다는 제한이 있지만 설정이 간단해서 사용한다.
// application.yml
spring:
mail:
host: smtp.gmail.com
port: 587
username: ${mail.username}
password: ${mail.password}
properties:
mail:
smtp:
auth: true
timeout: 5000
starttls:
enable: true
스프링부트의 application 설정파일에 spring.mail 설정을 하게되면 스프링이 JavaMailSender 인터페이스의 구현체를 빈으로 주입해준다.
Email Service 추상화
Active profiles의 옵션에 따라 local이면 콘솔로 메일의 내용을 출력하고, dev이면 실제로 메일을 보내게 하고 싶다.
@Service
@RequiredArgsConstructor
@Transactional
public class AccountService implements UserDetailsService {
private final AccountRepository accountRepository;
private final JavaMailSender javaMailSender;
private final PasswordEncoder passwordEncoder;
// Controller + 재전송 호출 시 Managed Entity 넘기기
public void sendSignUpConfirmMail(Account newAccount) {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(newAccount.getEmail());
mailMessage.setSubject("구함, 회원 가입 인증");
mailMessage.setText("/check-email-token?token=" + newAccount.getEmailCheckToken() +
"&email=" + newAccount.getEmail());
javaMailSender.send(mailMessage); // ConsoleJavaMailSendor 구현체 이용
}
}
지금은 AccountService가 javaMailSendor 인터페이스를 받지만 AccountService 내의 로직이 콘솔로 구체화 콘솔로 출력하게끔 구현한 구현체밖에 사용이 불가하다.
@Data
@Builder
public class EmailMessage {
private String to;
private String subject;
private String message;
}
public interface EmailService {
void sendEmail(EmailMessage emailMessage);
}
메세지의 수신자, 제목, 메세지를 담는 DTO와 이메일 서비스를 추상화할 인터페이스를 만들고, 인터페이스를 콘솔로 출력, 이메일 보내게끔 구현화 한다.
@Component
@Profile("local")
@Slf4j
public class ConsoleEmailService implements EmailService{
@Override
public void sendEmail(EmailMessage emailMessage) {
log.info("sent email: {}", emailMessage.getMessage());
}
}
@Profile("dev")
@Component
@Slf4j
@RequiredArgsConstructor
public class HtmlEmailService implements EmailService{
private final JavaMailSender javaMailSender;
@Override
public void sendEmail(EmailMessage emailMessage) {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
try {
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(emailMessage.getTo());
mimeMessageHelper.setSubject(emailMessage.getSubject());
mimeMessageHelper.setText(emailMessage.getMessage(), true);
javaMailSender.send(mimeMessage);
log.info("sent email: {}" , emailMessage.getMessage());
} catch (MessagingException e) {
log.error("failed to send email", e);
}
}
}
인터페이스 구현체끼리는 Profile에 따라 다르게 AccountService에 주입된다.
Thymeleaf Context
실제 이메일로 보낼 때 메일의 본문을 HTML로 보낼때 타임리프를 이용할 수 있다.
// HTML Email View
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<p>안녕하세요. <span th:text="${nickname}" ></span>님</p>
<h2 th:text="${message}">메시지</h2>
<div>
<a th:href="${host+link}" th:text="${linkName}"></a>
<p>링크가 동작하지 않는 경우에는 아래 URL로 접속하세요</p>
<small th:text="${host+link}"></small>
</div>
</div>
<footer>
<small>팀빌딩 구함© 2023</small>
</footer>
</body>
</html>
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class AccountService implements UserDetailsService {
private final AccountRepository accountRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
private final TemplateEngine templateEngine;
// Controller + 재전송 호출 시 Managed Entity 넘기기
public void sendSignUpConfirmMail(Account newAccount) {
Context context = new Context(); // Model 같이 사용하기
context.setVariable("link", "/check-email-token?token=" + newAccount.getEmailCheckToken() +
"&email=" + newAccount.getEmail());
context.setVariable("nickname", newAccount.getNickname());
context.setVariable("linkName", "이메일 인증하기");
context.setVariable("message", "팀빌딩구함 서비스를 이용하려면 링크를 클릭하세요");
context.setVariable("host", appProperties.getHost());
String message = templateEngine.process("mail/simple-link", context); // View 생성 후 Model 주입
EmailMessage email = EmailMessage.builder()
.to(newAccount.getEmail())
.subject("구함, 회원 가입 인증")
.message(message)// HTML 본문
.build();
emailService.sendEmail(email);
}
타임리프의 TemplateEngine을 주입받아 타임리프 템플릿을 처리하고 렌더링할 수 있다.타임리프의 Context는 TemplateEngine에게 데이터를 전달하고 템플릿의 변수를 참조하거나 조작할 수 있도록 한다.Controller 에서 View로 넘어간 Model이 Context로 전환되어 타임리프 템플릿에서 사용하는 걸 서비스 계층에서 한다고 보면된다.
테스트시 Application 설정파일
application 설정파일을 분리하고 Edit Configuration을 통해 Active Profiles를 설정해도 테스트할 때는 별도의 설정을 거치지 않으면 메인 설정파일을 따라간다.
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard