본문으로 건너뛰기

결정-0016: 메일 발송은 SMTP 로 하고 비동기로 보낸다

상태

승인됨

맥락

MAIL-01(트랜잭션 이메일 발송)에 두 가지가 미결로 남아 있었습니다. 발송 인프라를 SMTP 로 할지 외부 서비스로 할지, 그리고 동기와 비동기 중 무엇으로 보낼지입니다. 결정-0015로 이메일 소유 증명을 OTP로 정하면서 메일이 가입 흐름의 임계 경로에 들어왔으므로 함께 정할 시점이 되었습니다.

당시 전제는 초기 트래픽 규모를 가정하지 않는다는 것입니다. MAIL-01이 이미 발송을 포트 뒤에 둘 것을 요구하고 있어 도메인이 발송 수단을 모르며, 인프라 교체는 어댑터 교체로 끝납니다.

검토한 대안

발송 인프라

안 내용
Gmail SMTP (채택) 별도 계약과 API 키 발급 없이 시작할 수 있습니다. 저장소를 받은 사람이 즉시 동작을 확인할 수 있는 것이 가장 큰 값입니다. 제약은 아래 결과 절에 적습니다
전용 발송 서비스 도달률과 발송량, 이벤트 웹훅이 우수합니다. 다만 계정 생성과 도메인 인증(SPF · DKIM · DMARC), 샌드박스 해제가 선행되어야 하며 첫 실행 경험을 무겁게 만듭니다

포트 뒤에 있으므로 이 선택은 되돌리기 쉬운 결정입니다. 되돌리기 쉬운 결정은 가장 빨리 시작할 수 있는 쪽으로 정합니다.

동기와 비동기

안 내용
동기 발송 결과를 응답에 반영할 수 있습니다. 그러나 SMTP 는 TLS 핸드셰이크와 릴레이로 수백 밀리초에서 수 초가 걸리고, 그 지연이 그대로 가입 API의 응답 시간이 됩니다. 외부 인프라의 지연을 자기 응답 시간 목표로 들이는 셈입니다
비동기 (채택) 응답을 먼저 돌려줍니다. OTP 흐름에서는 사용자가 이미 코드 입력 화면으로 넘어가 메일을 기다리는 중이므로 응답을 먼저 주는 편이 흐름에 맞습니다
outbox 테이블과 워커 at-least-once 전달을 보장합니다. 다만 OTP 유실은 재발송으로 복구되므로 현 단계에서 지불할 복잡도가 아닙니다. 필요해지는 시점의 업그레이드 경로로 둡니다

결정

  1. 발송 어댑터는 SMTP 로 구현합니다. MAIL-01이 요구하는 포트 뒤에 두어 전용 발송 서비스로 교체할 때 어댑터와 설정만 바꾸면 되게 합니다.
  2. 발송은 비동기로 합니다. 전용 executor 를 두고 공용 풀을 쓰지 않으며, 유스케이스 응답이 발송을 기다리지 않게 합니다.
  3. 발송은 트랜잭션 커밋 이후에 트리거합니다. 트랜잭션 안에서 보내면 롤백 시 유령 메일이 나갑니다.
  4. 실패는 유스케이스를 중단시키지 않습니다. 실패를 로그와 지표로 남기고 사용자 측 복구 수단은 재발송입니다. 재시도는 짧은 횟수로 제한합니다.
  5. 자격증명은 환경 변수로 주입합니다. 앱 비밀번호를 저장소에 넣지 않습니다.
  6. 본문과 제목은 템플릿으로 조립하고 사용자 입력을 이스케이프합니다. 헤더와 내용 인젝션을 막기 위함이며, 수신자 주소에 개행이 들어갈 수 없게 검증합니다.

재검토 조건

  • 발송량이 한도에 접근하거나 도달률이 문제가 되는 경우 어댑터를 전용 발송 서비스로 교체합니다. 포트가 있으므로 도메인 코드는 바뀌지 않습니다.
  • 메일 유실이 재발송으로 감당되지 않는 종류의 알림이 생기는 경우 outbox 테이블과 워커를 도입합니다. 결제 영수증이 그 예입니다.
  • 발신 도메인을 서비스 도메인으로 바꿔야 하는 경우 전용 발송 서비스로 옮깁니다. SMTP 는 발신자가 계정 주소로 고정되기 때문입니다.

결과

  • 저장소를 받은 사람이 계정 하나와 앱 비밀번호만으로 메일 흐름을 끝까지 확인할 수 있습니다.
  • 가입 API의 응답 시간이 SMTP 지연과 분리됩니다.
  • 감수하는 것
    • 앱 비밀번호가 필요합니다. 2단계 인증이 켜진 계정이어야 합니다.
    • 발신자 주소가 그 계정 주소로 고정됩니다. 서비스 도메인 발신은 전용 서비스로 옮겨야 합니다.
    • 일일 발송 한도가 있습니다. 무료 계정 약 500통, 업무용 계정 약 2000통이며 프로덕션 규모에는 부족합니다.
    • 발송 실패를 사용자에게 즉시 알릴 수 없습니다. 재발송 수단이 항상 열려 있어야 합니다.
    • at-least-once 보장이 없습니다. OTP와 검증 메일에 한정해 재발송으로 갈음합니다.
© 2026 dev.goraebap