
비슷한 고민을 한 사람들이 이미 많았다.
@Async 적용 방법 전반을 잘 정리해놓은 글. SMTP 같은 네트워크 I/O 작업에 적용하는 게 대표적인 사례로 나온다.두 글을 보면서 "메일처럼 외부 네트워크를 타는 작업은 유저 응답 흐름 밖으로 빼는 게 맞다"는 확신이 생겼다.

Virgin Road를 직접 써보다가 불편한 게 느껴졌다.
방 만들기 버튼을 누르면 인증 코드 메일이 발송되는데, 버튼을 누르고 나서 화면이 잠깐 멈추는 느낌이 있었다. 처음엔 그냥 넘겼는데, 여러 번 써보니까 계속 신경 쓰였다.
코드를 보니 원인이 바로 보였다.
메일 발송이 동기로 실행되고 있었다.
유저가 버튼 클릭
→ Redis에 인증 코드 저장
→ Gmail SMTP 서버에 연결
→ 메일 전송
→ SMTP 응답 대기 ← 여기서 유저가 기다림
→ 그제야 API 응답 반환
Gmail SMTP는 외부 네트워크를 타는 작업이다. 빠를 때는 괜찮지만 네트워크 상태에 따라 수백 ms에서 길면 수 초까지 걸린다. 유저 입장에서는 버튼 눌렀는데 화면이 굳어버린 것처럼 느껴지는 순간이다.
사실 메일 발송이 끝날 때까지 유저를 기다리게 할 이유가 없다. Redis에 인증 코드 저장이 완료된 순간 이미 할 일은 다 한 거고, 메일은 그냥 알림 수단일 뿐이다.
메일 발송을 별도 스레드에 위임하도록 비동기로 바꿨다.
먼저 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 연결
→ 메일 전송

@Async를 붙이면 메일 발송 실패가 유저한테 전달되지 않는다. SMTP가 터져도 API는 성공 응답을 내려준다. 즉 유저는 "인증 코드 발송됨" 화면을 봤는데 메일이 안 온 상황이 생길 수 있다.
처음엔 이게 좀 찜찜했는데, 생각해보니 이미 해결책이 있었다. 인증 화면에 재발송 버튼이 있다. 메일이 안 왔으면 다시 받으면 되고, 이게 사실 어느 서비스든 이메일 인증에서 다 쓰는 패턴이다.
메일 발송 실패 하나 때문에 전체 응답을 느리게 만드는 건 그냥 손해다.
| 변경 전 | 변경 후 | |
|---|---|---|
| 메일 발송 방식 | 동기 (SMTP 응답 대기) | 비동기 (별도 스레드) |
| API 응답 시점 | SMTP 완료 후 | 즉시 |
| 발송 실패 영향 | API 에러 응답 | 유저 모름 (재발송으로 커버) |
외부 네트워크를 타는 작업은 유저 응답 흐름 밖으로 빼는 게 맞다. 메일뿐 아니라 Slack 알림, 푸시 알림 같은 것들도 동일하게 적용할 수 있는 패턴이다.