[Spring] SMTP와 비동기

Yunkeun
11 min readAug 5, 2022

--

프로젝트에서 회원가입 시 메일 인증을 위해 서버에서 메일을 보내는 기능이 필요하다.

해당 기능을 사용하기 위해 SMTP (간이 우편 전송 프로토콜)에 대해 알아보고 사용해보자.

SMTP (Simple Mail Transfer Protocol)

메일 전송에 사용되는 네트워크 프로토콜로 인터넷에서 메일 전송에 사용되는 표준이다.

메일을 송수신하는 서버를 SMTP 서버라고 한다.

TCP 프로토콜을 사용하며 포트는 다음과 같다.

  • 기본 TCP 포트: 25
  • TLS 포트: 587
  • SSL 포트: 465

SMTP 서버

메일을 보내는 사람: username@gmail.com

메일 주소에서 username@gmail.com는 SMTP 클라이언트가 될 것이고 gmail.com 주소는 메일을 송수신하는 SMTP 서버가 될 것이다. (gmail의 SMTP 서버 주소는 smtp.gmail.com)

SMTP 메일 처리 방식

송신자 → 매체(전달자) → 수신자의 메시지 전송 과정을 따른다.

송신자는 MUA(Mail User Agent — 메일 클라이언트 프로그램)를 이용해 메일을 만들어 보낸다.

MUA는 TCP 587번 포트를 이용해 메일을 전송한다.

메일 서비스 제공자는 587번 포트를 제공하는데, 기존 방식과의 호환을 위해 25번 포트도 함께 제공한다.

  • MSA(Mail Submission Agent)는 587번 포트를, MTA(Mail Transfer Agent)는 25번 포트를 사용한다.
  • MSA와 MTA 모두 SMTP를 이용한다.
  • 차이점은 MSA는 인증을 제공하고 MTA는 인증을 제공하지 않는다.

메일 주소는 username@gmail.com 형식으로 구성된다.

메일을 받은 MTA는 도메인 이름을 찾아야하는데, DNS 서버로 부터 MX(Mail eXchanger) 레코드값을 이용하여 메일을 전송해야 하는 호스트를 찾을 수 있다.

메일이 수신지로 전달 됐으면 도착한 메일을 분류하여 사용자 메일함에 저장하는 등의 작업이 필요하다.

  • 이것은 MDA(Mail Delivery Agent)가 한다.

SMTP 통신 과정

스프링에서 메일 보내기

1. Google SMTP 사용을 위한 gmail 계정 생성

  1. 개인용 계정 생성 (username)
  2. 보안 수준이 낮은 앱 및 Google 계정에 대한 설정

2–2. 생성된 앱 비밀번호 (password)

- 위 설정을 해주지 않는다면 다음과 같은 에러가 발생할 것이다.

2. 스프링 설정

  1. 의존성 추가 (Gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
}

Google SMTP 서버를 사용하기 위해 applicaiton.yml 설정 파일에 다음과 같이 추가한다.

spring:
mail:
host: smtp.gmail.com
port: 587
username:
password:
properties:
mail:
smtp:
auth: true
starttls:
enable: true

2–1. username과 password

  • username에는 위 1–1에서 생성한 계정 메일 주소(****@gmail.com)를 입력하고
  • password에는 위 1–2–2에서 생성된 앱 비밀번호를 입력한다.

2–2. 개인정보

  • 개인정보가 입력되므로 꼭 프로젝트 공유 시 주의해야한다.
  • git에 업로드 시 꼭 gitignore에 설정 파일을 등록하여 개인정보 유출에 방지한다.

3. 전송 기능 구현

전체 코드는 깃허브를 참고하자

  • 회원가입 과정에서 메일 인증 기능을 구현할 계획이다.
  • 메일과 랜덤으로 생성한 코드, 계정에 대한 인증 여부가 필요하다.
  • 단순한 텍스트만 전송할 계획이므로 큰 그림을 그려보면 다음과 같다.

메일 전송 로직

  1. 클라이언트 측에서 보낸 메일 주소를 receiver 로 설정
  2. 메일 주소 검증
  • 프로젝트에서는 학교 계정만 가능하므로 학교 계정 여부에 따른 예외처리를 한다.
  • 이미 사용중인 메일에 대한 검증을 한다.

3. 랜덤 코드를 생성한다.

4. 전송할 메일의 subject와 text, 랜덤 코드를 입력한다.

5.메일을 전송한다.

  • 메일 검증 로직
  1. 클라이언트 측에서 보낸 메일 주소와 코드를 받는다.
  2. 이전에 저장하고 있던 메일 주소와 코드가 맞는지 확인한다.
  • 맞다면 해당 계정에 대해 인증 여부를 true로 변경한다.

3. 유저 엔티티에 메일 정보를 저장한다.

3–1. 메일 전송 코드

@Service
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender javaMailSender; public void sendMail(String mailAddress) {
checkPossibleMail(mailAddress);
SimpleMailMessage message = new SimpleMailMessage();
setMessage(message, mailAddress);
javaMailSender.send(message);
}

메일 전송을 위한 코드이다.

  • checkPossibleMail(): 요청된 메일 주소를 check 한다.
  • setMessage(): 메일에 담을 메시지를 set 한다.

즉, SimpleMailMessage에 누구에게 보낼지(to), 제목(subject), 내용 (text)을 set하여 javaMailSender.send() 메서드에 담아 보내기만 하면 된다.

4. 비동기 전송

  • 3번에서 메일 인증 기능을 구현하기 위해 우리는 외부에서 메일 보내는 서비스를 사용했다.
  • 실제로 기능을 실행해보면, 실행 시점에서 메일이 전송될 때까지 사용자 입장에서는 불편함을 느낄 수 있을만한 로딩 시간이 생긴다. (3.52s)

만약 외부 서비스에 문제가 있다하면 그 로딩 시간은 더 길어질 것이다.

이렇게 되면, 우리는 메일 전송이라는 외부 서비스 때문에 프로그램 전체에 영향을 받을 것이다.

따라서, 해당 스레드가 메일 전송을 기다리는 것이 아닌, 메일 전송 요청을 보내고 다른 일을 하게끔 비동기로 사용하고자 한다.

4–1. 설정 파일 작성

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
private static final Logger logger = LoggerFactory.getLogger(AsyncConfig.class); @Override
@Bean(name = "mailExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("MailExecutor");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
logger.error("Exception handler for async method '" + method.toGenericString()
+ "' threw unexpected exception itself", ex);
}
}

Spring에서는 @EnableAsync 옵션만 추가해도 비동기 처리를 사용할 수 있다.

이것을 커스터마이징하기 위해서는 위와 같이 설정 파일을 만들어 준다.

  • CorePoolSize: 기본적으로 실행 대기 중인 thread 개수
  • MaxPoolSize: 동시에 동작하는 최대 thread 개수
  • QueueCapacity: CorePool의 크기를 넘어설 때 저장되는 큐의 최대 용량

별도로 설정하지 않는다면 다음과 같은 default값을 가진다.

  • CorePoolSize = 1
  • MaxPoolSize = Integer.Max_VALUE
  • QueueCapacity = Integer.Max_VALUE

기본적으로 Spring은 SimpleAsyncTaskExecutor 를 사용하여 메서드를 비동기적으로 실행한다.

  • SimpleAsyncTaskExecutor 는 요청이 오는대로 계속해서 thread를 생성한다.

이때, Executor 타입의 빈이 한개라면, 해당 빈으로 작동한다. 하지만, 여러 개의 빈으로 등록한다면, SimpleAsyncTaskExecutor가 돌아가므로 이름을 통해 명시해주어야 한다.

Spring에서 @Async는 Future를 구현하는 AsyncResult 클래스를 제공한다. 이것은 비동기 메서드 실행 결과를 추적하는데 사용한다. 이때, 리턴 타입이 Future인 경우 예외 처리가 용이하다.

  • Future.get() 메서드는 예외를 처리한다.

리턴 타입이 void인 경우 예외가 호출 thread로 전파되지 않는다. 따라서 AsyncUncaughtExceptionHandler 인터페이스를 구현하여 비동기 예외 핸들러를 구현해야 한다.

4–2. @Async

@Async("mailExecutor")
public void sendMail(String mailAddress) {
checkPossibleMail(mailAddress);
SimpleMailMessage message = new SimpleMailMessage();
setMessage(message, mailAddress);
javaMailSender.send(message);
}

단지 @Async 어노테이션 하나만 붙여주면 한 thread 에서는 성공 응답값을 보내주고 다른 thread 에서는 메일을 전송하고 있을 것이다.

비동기로 메일을 보낼 경우 다음과 같이 119ms로 로딩 시간이 헌저히 줄어든 것을 확인할 수 있다.

5. 결론

이렇게 SMTP와 스프링에서 SMTP를 이용한 메일 전송, 비동기로 메일 전송하는 것까지 알아보았다.

정리하자면,

SMTP는 메일 전송에 사용되는 네트워크 프로토콜이며 SMTP에서 메일은 SMTP 서버를 통해 처리된다.

스프링에서 SMTP 사용을 위해 계정 설정 이후 스프링에서 의존성 추가 및 야믈 파일에 설정을 추가한다.

스프링에서 비동기 사용을 위해선 @EnableAsync 옵션만 추가해도 비동기 처리가 가능하며 필요에 따른 설정 파일 커스터마이징 후 메서드에 @Async 어노테이션을 추가하여 사용할 수 있다.

ref.

https://www.joinc.co.kr/w/Site/Network_Programing/Documents/SMTP

https://www.blog.ecsimsw.com/entry/Spring-Mail-Google-Smtp-server-Async

https://tecoble.techcourse.co.kr/post/2021-07-31-email-async-event/

https://king-veloper.tistory.com/15

https://bepoz-study-diary.tistory.com/399

--

--