시퀀스 이메일 발송 — 성능 개선 리포트

2026-06-17 · 대상: 앱린다(send-grid-test) · 환경: PostgreSQL 18.3 / Redis 8.6.1 · 실서버(beta) 실측 기반

이메일 자동발송에서 가장 무거웠던 작업(전체 DB 부하의 69.6%)을 실서버 기준 약 14배 빠르게 만들었습니다. 보내는 메일 결과는 100% 그대로, 속도만 개선했습니다.

핵심 수치 (실서버 beta 실측)

핵심 확인 작업 평균
91.6ms → 6.6ms
약 14× 빠름
서버 읽기 데이터(I/O)
10,597 → 860 blk
92% 감소
최악 계정 인덱스 접근
6,944 → 3 buffers
약 2,300× 감소
실행시간(cold/warm)
33.8→5.8 / 12.7→6.7ms
5.8× / 1.9×

무엇이 문제였나

시스템은 계정마다 “지금 보낼 메일이 있는지”를 쉴 새 없이 확인합니다(하루 수백만 번). 그런데 오래된 계정일수록 이미 발송이 끝난 기록 수십만 건을 매번 처음부터 훑은 뒤 버리고 있었습니다 — 헛수고가 반복됐습니다.

기술: 발송 대상 테이블 sequence_enrollments의 인덱스가 (user_email_account_id) 단일 컬럼이라, status='active' 조건을 인덱스가 아닌 데이터 본문에서 걸러내고 있었음(heap filter). 베테랑 계정일수록 비active 행을 대량 스캔 후 폐기(예: active 0건인데 10.8만 행 스캔).

무엇을 바꿨나

‘발송중인 것’만 곧장 찾도록 색인 기준을 바꿨습니다. 발송이 끝난 계정은 즉시 건너뜁니다.

기술: 인덱스를 복합 B-tree (user_email_account_id, status)로 교체. 발송 조건을 인덱스 단계에서 바로 좁혀(Index Condition), 0-active 계정은 Bitmap Index Scan으로 즉시 종료. 단일 인덱스는 prefix로 완전 대체되어 제거. 무중단 적용(CREATE INDEX CONCURRENTLY) + idempotent migration(0484).

추가로 고친 것 — 안정성 버그 완료

메일 발송 횟수를 세는 부분에, 서버가 갑자기 꺼지면 카운트가 영구히 잘못 남는 드문 오류가 있었는데 함께 제거했습니다.

기술: rate-limit·circuit-breaker의 INCR+EXPIRE 사이 crash 시 TTL 누락(영구 카운터) race를 단일 Redis Lua(EVAL)로 원자화. RTT 2→1, 4→1 동시 감소. beta Redis 8.6.1에서 동작 검증.

양쪽 서버 검증 결과

검증 항목alphabeta
복합 인덱스 실트래픽 사용(scan↑)16,629 ↑30,512 ↑
옛 단일 인덱스 제거
로더 쿼리 새 인덱스 사용
migration 0484✅ applied⏳ 정식 sync (DB 인덱스 live)
Redis fix 배포 · 에러✅ 배포 · 에러 0⏳ sync (Lua 검증완료)
회귀(부작용)00

인덱스 scan 카운트가 양쪽 모두 계속 증가 = production 트래픽이 실제로 새 인덱스를 타고 있음.

안전성 — 위험 변경 4건은 테스트로 차단

  • skip-scan 인덱스 제거 — alpha에서 scheduled_idx 95,907회·idx_step_contents_lead 372,536회 사용 중 → 회귀 위험
  • has_pending_work 게이팅 — beta에 컬럼 부재 + false-negative 시 발송 누락(치명)
  • leads join 지연 — 실측 효과 0(버퍼 동일)
  • legacy 로더 제거 — per-account-send 마이그 dual-run 중

→ 모든 적용은 실서버·테스트서버 양쪽 dual-DB 실측으로 검증, 결과 동일·회귀 0인 것만 반영.

남은 부분 별도 트랙

항목비중이유
#2/#3 전역 로더17.7%코드 구조(마이그 dual-run)+데이터 정합 → 팀 트랙(마이그 완료·reconciler)
admin·funnel·cleanup 쿼리~0.6%저빈도 + 대상 테이블 작거나 이미 적절히 색인됨 → 안전한 신규 개선점 없음