메일이 왜 이렇게 느려? - @Async로 비동기 처리하기

Junyoung·2026년 3월 26일

Virgin Road

목록 보기
5/5
post-thumbnail

참고한 레퍼런스

비슷한 고민을 한 사람들이 이미 많았다.

두 글을 보면서 "메일처럼 외부 네트워크를 타는 작업은 유저 응답 흐름 밖으로 빼는 게 맞다"는 확신이 생겼다.


문제를 발견한 순간

Virgin Road를 직접 써보다가 불편한 게 느껴졌다.

방 만들기 버튼을 누르면 인증 코드 메일이 발송되는데, 버튼을 누르고 나서 화면이 잠깐 멈추는 느낌이 있었다. 처음엔 그냥 넘겼는데, 여러 번 써보니까 계속 신경 쓰였다.

코드를 보니 원인이 바로 보였다.


문제의 원인

메일 발송이 동기로 실행되고 있었다.

유저가 버튼 클릭
  → Redis에 인증 코드 저장
  → Gmail SMTP 서버에 연결
  → 메일 전송
  → SMTP 응답 대기  ← 여기서 유저가 기다림
  → 그제야 API 응답 반환

Gmail SMTP는 외부 네트워크를 타는 작업이다. 빠를 때는 괜찮지만 네트워크 상태에 따라 수백 ms에서 길면 수 초까지 걸린다. 유저 입장에서는 버튼 눌렀는데 화면이 굳어버린 것처럼 느껴지는 순간이다.

사실 메일 발송이 끝날 때까지 유저를 기다리게 할 이유가 없다. Redis에 인증 코드 저장이 완료된 순간 이미 할 일은 다 한 거고, 메일은 그냥 알림 수단일 뿐이다.


@Async 적용

메일 발송을 별도 스레드에 위임하도록 비동기로 바꿨다.

먼저 AsyncConfig.java를 따로 만들었다.

@EnableScheduling이 있는 SchedulingConfig.java에 같이 넣을 수도 있었지만, 스케줄링이랑 비동기는 역할이 다르다. 나중에 코드 볼 때 "여기서 왜 @EnableAsync가 나와?" 싶은 상황을 만들고 싶지 않았다.

@Configuration
@EnableAsync
public class AsyncConfig {
}

유저 요청 흐름에 있는 메서드에만 @Async를 붙였다.

@Async
public void sendVerificationCode(String email, String code) { ... }

@Async
public void sendRoomCreated(String email, String groomName, String brideName, String shareUrl) { ... }

스케줄러에서 호출하는 sendLettersPdf, sendSchedulerReport는 건드리지 않았다. 어차피 유저가 기다리는 흐름이 아니니까.

적용하고 나면 흐름이 이렇게 바뀐다.

유저가 버튼 클릭
  → Redis에 인증 코드 저장
  → 메일 발송을 별도 스레드에 위임
  → API 응답 즉시 반환  ← 유저는 바로 다음 화면으로

  (백그라운드에서)
  → Gmail SMTP 연결
  → 메일 전송

적용 후 UI


한 가지 고민

@Async를 붙이면 메일 발송 실패가 유저한테 전달되지 않는다. SMTP가 터져도 API는 성공 응답을 내려준다. 즉 유저는 "인증 코드 발송됨" 화면을 봤는데 메일이 안 온 상황이 생길 수 있다.

처음엔 이게 좀 찜찜했는데, 생각해보니 이미 해결책이 있었다. 인증 화면에 재발송 버튼이 있다. 메일이 안 왔으면 다시 받으면 되고, 이게 사실 어느 서비스든 이메일 인증에서 다 쓰는 패턴이다.

메일 발송 실패 하나 때문에 전체 응답을 느리게 만드는 건 그냥 손해다.


정리

변경 전변경 후
메일 발송 방식동기 (SMTP 응답 대기)비동기 (별도 스레드)
API 응답 시점SMTP 완료 후즉시
발송 실패 영향API 에러 응답유저 모름 (재발송으로 커버)

외부 네트워크를 타는 작업은 유저 응답 흐름 밖으로 빼는 게 맞다. 메일뿐 아니라 Slack 알림, 푸시 알림 같은 것들도 동일하게 적용할 수 있는 패턴이다.

profile
라곰

0개의 댓글