영업을 넘겨받으면서 고객에게 개인정보 이전 사실을 알리는 메일을 보내야 했다. 대상은 약 96만 명이고, 발송은 Amazon SES 로 새로 구축했다. 첫 회차로 1,000통을 보냈더니 영구 반송이 19% 였다. SES 는 반송률이 높은 계정을 검토하거나 발송을 멈출 수 있어서, 이 비율로 나머지를 보내면 통지 자체가 중간에 막힐 수 있었다.
Table of contents
Open Table of contents
원인 — 19% 는 표본 탓인가
처음 의심한 것은 표본이었다. 발송 순서가 가입이 오래된 회원부터였다면, 19% 는 전체가 아니라 앞쪽의 죽은 주소만 반영한 값일 수 있다. 그래서 첫 회차 1,000명과 대상 중간 구간 1,000명의 가입 연도 분포를 Firebase Auth 의 createdAt 으로 비교했다. 2019년 6.4% 대 6.2%, 2020년 20.0% 대 19.4%, 2021년 33.3% 대 32.7% 처럼 두 표본이 거의 같았다. 꺼내는 순서는 적재 순서이고, 원본이 무작위 문자열인 uid 순이라 가입일과 무관했다. 19% 는 목록 전체를 대표하는 값이었다.
다음으로 활동 회원만 먼저 보내면 되는지 봤다. 마지막 토큰 갱신(lastRefreshAt)이 2025년 이후인 회원은 4.7% 뿐이었고, 그들의 반송률도 10.6% 였다. 주문 이력 기준으로 활동 회원 86,014명을 뽑아 대조해도 반송률은 크게 줄지 않았다. 활동 회원만 보내는 안은 대상만 크게 줄이고 반송률은 낮추지 못한다. 문제는 누구에게 보내느냐가 아니라, 받는 메일함이 실제로 있느냐였다.
판단 — 보내 보고 거르기와 먼저 확인하기
남은 선택지는 다음 두 가지 방법이었다.
- 그대로 보내고, SES 가 돌려주는 반송으로 주소를 걸러 낸다
- 보내기 전에 각 메일 사업자의 받는 서버에 그 메일함이 있는지 확인하고, 없는 주소를 발송 대상에서 뺀다
1번은 반송이 생긴 뒤에 거르므로, 거르는 동안 쌓인 반송률이 그대로 계정 평판이 된다. 반면 2번은 메일을 보내지 않고 수신 서버의 응답만 보고 판정한다. 2번이 맞다고 본 이유는 반송률을 낮추는 유일한 길이 반송될 메일을 아예 보내지 않는 것이기 때문이다. 대가는 확인 자체에 시간이 들고, 확인할 수 없는 사업자가 남는다는 점이다.
해결 — 판정을 되돌릴 수 있게 표시한다
수신 서버의 응답 코드로 두 가지만 표시했다.
5.1.1메일함 없음 — 주소 단위로, 그 주소가 걸린 모든 대기 캠페인에서 뺀다5.2.1휴면 — 이 통지 캠페인에서만 뺀다
표시는 대기 행의 status 를 suppressed 로, error 를 rcpt:5.1.1 처럼 출처가 드러나는 값으로 바꾸는 것이다. 순서는 매번 같았다. 입력 id 와 DB 이메일의 도메인이 100% 일치하고 전부 대기 상태인지 검증하고, 바꿀 행을 TSV 로 백업해 sha256 을 남기고, 한 트랜잭션으로 UPDATE 하되 갱신 수가 예상과 다르면 롤백하고, 끝나면 되읽었다. 발송 중인 행이 0 이고 발송 Job 이 멈춘 상태에서만 갱신했고, 갱신은 대기 행만 건드렸다.
되돌리는 방법은 처음부터 한 줄로 정해 두었다.
UPDATE mail_targets SET status='pending', error=NULL
WHERE status='suppressed' AND error LIKE 'rcpt:%';
rcpt: 로 시작하는 것만 풀어서, SES 반송으로 억제한 주소는 건드리지 않는다. 받는 메일함 확인은 사업자별로 길게 돌았으므로 결과를 증분으로 붙였다. 마지막 반영 뒤 누적은 메일함 없음 18,714건, 휴면 50,802건이었다.
증분을 붙이다 함정도 하나 밟았다. 입력을 고정하는 기준 시각을 미래로 잡자 검증·백업·적용 사이에 입력이 계속 늘어 건수가 어긋났다. 지금 시각을 읽고 지난 시각으로 고정한 뒤 같은 값으로 세 단계를 돌려야 한다. 백업 명령이 연결이 끊겨 rc=1 로 끝났을 때는 백업이 잘린 것이므로 적용하지 않고 다시 받았다.
영향 범위 — 판정의 한계
판정에는 한계가 있었고 그대로 적었다. 휴면 판정이 실제로는 받을 수 있는 주소를 빼는 거짓 제외는 한 사업자 대조 137건 기준으로 최대 3.9% 수준이었다. 확인할 수 없는 사업자도 있었다. 한 곳은 표본 6건만 확인됐고, 한 곳은 역방향 DNS 검사(FCrDNS)로 확인을 거부했고, 한 곳은 응답이 없었다. 그 주소들은 판정이 없으므로 표시하지 않았다. MX 레코드가 없는 도메인 4,506건과 다른 거절 코드는 승인 범위 밖이라 표시하지 않고 따로 결정을 받기로 했다.
결과 — 먼저 확인하고 뺀 주소가 지킨 평판
- 발송 계정의 평판. 메일함 없음 18,714건과 휴면 50,802건을 보내기 전에 대상에서 빼, 그만큼의 확정적 반송을 만들지 않았다
- 되돌릴 수 있는 판정. 표시를 출처가 드러나는 값으로 남기고 되돌리는 SQL 한 줄을 먼저 정해, 판정이 틀려도 그 표시만 풀 수 있게 했다
- 남긴 위험. 휴면 판정의 거짓 제외는 최대 3.9% 수준이고, 확인할 수 없는 사업자의 주소는 판정 없이 남았다
정리
- 반송률이 높으면 표본 편향부터 가른다. 순서가 무작위인지, 가입 연도 분포가 같은지 보면 대표성을 판정할 수 있다
- 반송률을 낮추는 길은 반송될 메일을 보내지 않는 것이다. 활동 회원만 고르는 것으로는 반송률이 내려가지 않았다
- 발송 대상에서 빼는 표시는 출처가 드러나는 값으로 남기고, 되돌리는 한 줄을 먼저 정한다
- 입력을 고정하는 기준 시각은 지난 시각으로 잡는다. 미래 시각으로 잡으면 검증과 적용이 다른 입력을 본다