Spring

[SpringBoot] 구글 SMTP 통해 메일 보내기

꾸준함. 2022. 4. 10. 00:05

개요

회원 가입 이후 이메일 인증을 한 사용자에게만 서비스 접근을 할 수 있도록 구현하고 싶어 찾아본 결과 구글에서 제공하는 SMTP 서비스를 통해 비교적 쉽게 구현할 수 있었습니다.

단, 구글 SMTP 서비스는 한 이메일 당 하루 100건씩 제한을 걸기 때문에 로컬에서 여러 번 테스트하고 싶을 때는 실제 메일을 보내지 않고 콘솔에 메일 내용을 로그로 작성하는 것을 추천드립니다.

이번 게시글에서는 구글 SMTP 설정과 메일 보내는 방법 그리고 콘솔에 메일 내용을 로그로 작성하는 방법을 간단히 공유해보겠습니다.

 

1. 구글 계정 앱 비밀번호 설정 및 application.properties 설정

구글 SMTP를 사용하기 위해서는 구글 계정 내 앱 비밀번호를 설정해야 합니다.

https://support.google.com/mail/answer/185833

 

앱 비밀번호로 로그인 - Gmail 고객센터

도움이 되었나요? 어떻게 하면 개선할 수 있을까요? 예아니요

support.google.com

 

요약을 하자면 아래와 같습니다.

  • Google 계정 관리 > 보안 > Google에 로그인 > 2단계 인증 설정
  • Google 계정 관리 > 보안 > Google에 로그인 > 앱 비밀번호 설정
    • 앱 선택 > 기타 > SMTP로 설정 후 생성
    • 생성한 비밀번호를 복사

 

위 과정을 모두 거친 뒤에는 아래와 같이 SpringBoot 내 application.properties 혹은 application.yml을 설정해주시면 됩니다.

 

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=[본인 이메일]
spring.mail.password=[복사한 비밀번호]
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.starttls.enable=true

 

2. MailService 구현

application.properties를 설정했다면 이제 실제 메일을 보내는 서비스를 구현하면 됩니다.

MailService는 아래와 같이 구현하면 됩니다.

 

@Slf4j
@Profile("dev")
@Component
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender javaMailSender;
public void send(EmailMessage emailMessage) {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
try {
/**
* 첨부 파일(Multipartfile) 보낼거면 true
*/
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(emailMessage.getTo());
mimeMessageHelper.setSubject(emailMessage.getSubject());
/**
* html 템플릿으로 보낼거면 true
* plaintext로 보낼거면 false
*/
mimeMessageHelper.setText(emailMessage.getMessage(), true);
javaMailSender.send(mimeMessage);
log.info("sent email: {}", emailMessage.getMessage());
} catch (MessagingException e) {
log.error("[EmailService.send()] error {}", e.getMessage());
}
}
}
@Data
@Builder
public class EmailMessage {
// 수신자
private String to;
// 제목
private String subject;
// 메시지
private String message;
}
view raw .java hosted with ❤ by GitHub

 

  • org.springframework.mail.javamail의 JavaMailSender를 Autowired 받고 해당 객체를 통해 MimeMessage를 전송
  • 메일에 텍스트와 함께 이미지를 같이 보내기 위해서는 MimeMessageHelper 생성자의 두 번째 인자로 true 설정
  • html 템플릿을 통해 메시지를 보낼 거면 setText() 메서드의 두 번째 인자로 true 설정, 그냥 String으로 보낼거면 false 설정

 

2.1 html 템플릿 예시

타임리프를 pom.xml 혹은 gradle에 추가할 경우 TemplateEngine 객체를 Autowired 받을 수 있습니다.

TemplateEngine을 통해 Context를 설정하고 html 템플릿과 연동하면 html 템플릿을 메일 메시지로 보낼 수 있습니다.

 

html

 

<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>HTML 템플릿 메일 메시지 예시</title>
</head>
<body>
<div>
<p>안녕하세요 <span th:text="${name}"></span>님</p>
<h2 th:text="${message}">메시지</h2>
</div>
</body>
</html>
view raw .html hosted with ❤ by GitHub

 

예시 Service


@Service
@Slf4j
@Transactional
@RequiredArgsConstructor
public class ExampleService {
private static final String EXAMPLE_LINK_TEMPLATE = "템플릿 경로";
private final TemplateEngine templateEngine;
private final EmailService emailService;
public void sendLoginLink(Account account) {
Context context = getContext(account);
String message = templateEngine.process(EXAMPLE_LINK_TEMPLATE, context);
EmailMessage emailMessage = EmailMessage.builder()
.to(account.getEmail())
.subject("이메일 제목")
.message(message)
.build();
emailService.send(emailMessage);
}
private Context getContext(Account account) {
Context context = new Context();
context.setVariable("name", account.getName());
context.setVariable("message", "메일 메시지");
return context;
}
}
view raw .java hosted with ❤ by GitHub

 

2.2 메일 전송 악용할 경우

앞서 개요에도 언급했다시피 구글 SMTP는 한 계정당 하루 100건으로 메일 전송을 제한합니다.

또한, sendgridmailgun과 같이 제한이 없는 서비스를 사용하더라도 사용자가 메일을 짧은 시간 내 여러 번 보낼 수 있게 되면 트래픽 과부하가 발생해 서비스에 지장을 줄 수가 있습니다.

따라서, 이를 방지하기 위해 동일한 메일에 대해서는 해당 계정으로 최근 x분 혹은 x시간 내 보낸 적이 없어야 재전송할 수 있도록 설정을 해주는 것이 안전합니다.

이를 위해서는 아래와 같이 Account 객체 내 토큰 필드와 함께 토큰 필드 생성 시간 필드를 만들고 이메일 전송 가능 여부를 판별해주는 boolean 메서드를 구현하면 됩니다.

 

* 토큰 필드는 인증 메일 확인용으로 생성된 것인데 이 부분은 생략하도록 하겠습니다.

* 핵심 포인트는 생성 시간 필드를 현재 시간과 비교하여 이메일 전송 가능 여부를 확인하는 것입니다.

 

Account.java


@Entity
@Getter
@Setter
@EqualsAndHashCode(of = "id")
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Account {
// 중략
private String name;
private String email;
private String emailCheckToken;
private LocalDateTime emailCheckTokenGeneratedAt;
public void generateEmailCheckToken() {
this.emailCheckToken = UUID.randomUUID().toString();
this.emailCheckTokenGeneratedAt = LocalDateTime.now();
}
public boolean isValidToken(String token) {
return this.emailCheckToken.equals(token);
}
public boolean canSendConfirmEmail() {
return this.emailCheckTokenGeneratedAt.isBefore(LocalDateTime.now().minusHours(1));
}
}
view raw .java hosted with ❤ by GitHub

 

예시 Controller + Service


@Controller
@RequiredArgsConstructor
public class ExampleController {
private final ExampleService exampleService;
private final AccountRepository accountRepository;
@PostMapping("/email-login")
public String sendEmailLoginLink(String email
, Model model
, RedirectAttributes attributes) {
Account account = accountRepository.findByEmail(email);
if (account == null) {
model.addAttribute("error", "유효한 이메일 주소가 아닙니다.");
return "account/email-login";
}
if (!account.canSendConfirmEmail()) {
model.addAttribute("error", "이메일 로그인은 1시간 뒤에 사용할 수 있습니다.");
return "account/email-login";
}
exampleService.sendLoginLink(account);
attributes.addFlashAttribute("message", "이메일 인증 메일을 발송했습니다.");
return "redirect:/email-login";
}
}
@Service
@Slf4j
@Transactional
@RequiredArgsConstructor
public class ExampleService {
private static final String EXAMPLE_LINK_TEMPLATE = "템플릿 경로";
private final TemplateEngine templateEngine;
private final EmailService emailService;
public void sendLoginLink(Account account) {
account.generateEmailCheckToken();
Context context = getContext(account);
String message = templateEngine.process(EXAMPLE_LINK_TEMPLATE, context);
EmailMessage emailMessage = EmailMessage.builder()
.to(account.getEmail())
.subject("이메일 제목")
.message(message)
.build();
emailService.send(emailMessage);
}
private Context getContext(Account account) {
Context context = new Context();
context.setVariable("name", account.getName());
context.setVariable("message", "메일 메시지");
return context;
}
}
view raw .java hosted with ❤ by GitHub

 

3. local 테스트용으로 이메일 내용을 로그만 찍는 방법

앞서 설명한 방법은 SMTP를 통해 실제 메일을 보내는 방법이고 지금부터 작성할 내용은 JavaMailSender를 통해 메일 내용을 로그로 찍는 방법입니다.

구조는 똑같다고 보면 되고 유일하게 다른 점은 MimeMessage가 아닌 SimpleMailMessage 객체를 사용한다는 것입니다.

 

ConsoleMailSender.java


@Profile("local")
@Component
@Slf4j
public class ConsoleMailSender implements JavaMailSender {
@Override
public MimeMessage createMimeMessage() {
return null;
}
@Override
public MimeMessage createMimeMessage(InputStream contentStream) throws MailException {
return null;
}
@Override
public void send(MimeMessage mimeMessage) throws MailException {
}
@Override
public void send(MimeMessage... mimeMessages) throws MailException {
}
@Override
public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException {
}
@Override
public void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException {
}
@Override
public void send(SimpleMailMessage simpleMessage) throws MailException {
log.info("[Mail Message] {}", simpleMessage.getText());
}
@Override
public void send(SimpleMailMessage... simpleMessages) throws MailException {
}
}
view raw .java hosted with ❤ by GitHub

 

ExampleService.java


@Service
@Slf4j
@Transactional
@RequiredArgsConstructor
public class ExampleService {
private final JavaMailSender javaMailSender;
public void sendLoginLink(Account account) {
// account.generateEmailCheckToken();
// 로그는 굳이 시간 제한을 주지 않아도 될 듯
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(account.getEmail());
mailMessage.setSubject("메일 제목");
mailMessage.setText("메일 제목");
emailService.send(emailMessage);
}
}
view raw .java hosted with ❤ by GitHub

 

참고

인프런 강의 - 스프링과 JPA 기반 웹 애플리케이션 개발 (백기선 강사님)

 

반응형