결정-0016: 메일 발송은 SMTP 로 하고 비동기로 보낸다
상태
승인됨
맥락
MAIL-01(트랜잭션 이메일 발송)에 두 가지가 미결로 남아 있었습니다. 발송 인프라를 SMTP 로 할지 외부 서비스로 할지, 그리고 동기와 비동기 중 무엇으로 보낼지입니다. 결정-0015로 이메일 소유 증명을 OTP로 정하면서 메일이 가입 흐름의 임계 경로에 들어왔으므로 함께 정할 시점이 되었습니다.
당시 전제는 초기 트래픽 규모를 가정하지 않는다는 것입니다. MAIL-01이 이미 발송을 포트 뒤에 둘 것을 요구하고 있어 도메인이 발송 수단을 모르며, 인프라 교체는 어댑터 교체로 끝납니다.
검토한 대안
발송 인프라
| 안 | 내용 |
|---|---|
| Gmail SMTP (채택) | 별도 계약과 API 키 발급 없이 시작할 수 있습니다. 저장소를 받은 사람이 즉시 동작을 확인할 수 있는 것이 가장 큰 값입니다. 제약은 아래 결과 절에 적습니다 |
| 전용 발송 서비스 | 도달률과 발송량, 이벤트 웹훅이 우수합니다. 다만 계정 생성과 도메인 인증(SPF · DKIM · DMARC), 샌드박스 해제가 선행되어야 하며 첫 실행 경험을 무겁게 만듭니다 |
포트 뒤에 있으므로 이 선택은 되돌리기 쉬운 결정입니다. 되돌리기 쉬운 결정은 가장 빨리 시작할 수 있는 쪽으로 정합니다.
동기와 비동기
| 안 | 내용 |
|---|---|
| 동기 | 발송 결과를 응답에 반영할 수 있습니다. 그러나 SMTP 는 TLS 핸드셰이크와 릴레이로 수백 밀리초에서 수 초가 걸리고, 그 지연이 그대로 가입 API의 응답 시간이 됩니다. 외부 인프라의 지연을 자기 응답 시간 목표로 들이는 셈입니다 |
| 비동기 (채택) | 응답을 먼저 돌려줍니다. OTP 흐름에서는 사용자가 이미 코드 입력 화면으로 넘어가 메일을 기다리는 중이므로 응답을 먼저 주는 편이 흐름에 맞습니다 |
| outbox 테이블과 워커 | at-least-once 전달을 보장합니다. 다만 OTP 유실은 재발송으로 복구되므로 현 단계에서 지불할 복잡도가 아닙니다. 필요해지는 시점의 업그레이드 경로로 둡니다 |
결정
- 발송 어댑터는 SMTP 로 구현합니다. MAIL-01이 요구하는 포트 뒤에 두어 전용 발송 서비스로 교체할 때 어댑터와 설정만 바꾸면 되게 합니다.
- 발송은 비동기로 합니다. 전용 executor 를 두고 공용 풀을 쓰지 않으며, 유스케이스 응답이 발송을 기다리지 않게 합니다.
- 발송은 트랜잭션 커밋 이후에 트리거합니다. 트랜잭션 안에서 보내면 롤백 시 유령 메일이 나갑니다.
- 실패는 유스케이스를 중단시키지 않습니다. 실패를 로그와 지표로 남기고 사용자 측 복구 수단은 재발송입니다. 재시도는 짧은 횟수로 제한합니다.
- 자격증명은 환경 변수로 주입합니다. 앱 비밀번호를 저장소에 넣지 않습니다.
- 본문과 제목은 템플릿으로 조립하고 사용자 입력을 이스케이프합니다. 헤더와 내용 인젝션을 막기 위함이며, 수신자 주소에 개행이 들어갈 수 없게 검증합니다.
재검토 조건
- 발송량이 한도에 접근하거나 도달률이 문제가 되는 경우 어댑터를 전용 발송 서비스로 교체합니다. 포트가 있으므로 도메인 코드는 바뀌지 않습니다.
- 메일 유실이 재발송으로 감당되지 않는 종류의 알림이 생기는 경우 outbox 테이블과 워커를 도입합니다. 결제 영수증이 그 예입니다.
- 발신 도메인을 서비스 도메인으로 바꿔야 하는 경우 전용 발송 서비스로 옮깁니다. SMTP 는 발신자가 계정 주소로 고정되기 때문입니다.
결과
- 저장소를 받은 사람이 계정 하나와 앱 비밀번호만으로 메일 흐름을 끝까지 확인할 수 있습니다.
- 가입 API의 응답 시간이 SMTP 지연과 분리됩니다.
- 감수하는 것
- 앱 비밀번호가 필요합니다. 2단계 인증이 켜진 계정이어야 합니다.
- 발신자 주소가 그 계정 주소로 고정됩니다. 서비스 도메인 발신은 전용 서비스로 옮겨야 합니다.
- 일일 발송 한도가 있습니다. 무료 계정 약 500통, 업무용 계정 약 2000통이며 프로덕션 규모에는 부족합니다.
- 발송 실패를 사용자에게 즉시 알릴 수 없습니다. 재발송 수단이 항상 열려 있어야 합니다.
- at-least-once 보장이 없습니다. OTP와 검증 메일에 한정해 재발송으로 갈음합니다.