국가별 영업일 발송 게이트
beta 실데이터 기반 최적 설계
수십만건 규모에서 "발송시 LLM으로 국가 판단" 우려를 실측으로 검증 — 비용·성능·오류 분석과 최적 아키텍처
00 결론
✅ 핵심
발송시점 LLM은 전혀 불필요.
sequence_step_executions.timezone이 이미 100% 저장(IANA 문자열, 잡 페이로드 동봉)돼 있어 발송 게이트는 $0 · 산술 연산으로 끝난다.
⛔ 진짜 문제
tz=
Asia/Seoul 발송의 87%(10,998건)가 "수신자국을 이미 알면서도" 한국으로 잘못 디폴트됨. LLM이 아니라 country→tz 정적 변환이 누락된 버그에 가깝다.
⚠️ 설계
수십만건 최적해 = LLM을 발송경로에서 완전히 제거하고, 비싼 국가판단은 리드당 1회 · 도메인 캐시로 amortize. per-send LLM은 회피 대상.
01 실측 데이터 (beta)
100%
execution.timezone 채움률 (2.83M건) — 발송시 국가판단 불요
87%
tz=Seoul인데 country=해외(기지) → 잘못 디폴트 (10,998/12,648)
94.5%
수신자 해외 비중 (한국 5.5%) — "한국기준" 부적합
97,216
sequence-email delayed 큐 (미래예약 대기)
delayed 97,216건이 KST 언제 발사되나 (전수 계산)
업무외(18–09 KST) 발사예정 49.4% — 단 상당부분은 해외 수신자 현지 낮시간을 노린 의도된 결과(기존 clampToBusinessHours가 수신자 tz 기준).
수신자 국가 분포 (60일 발송)
| 국가 | 비중 | 국가 | 비중 |
|---|---|---|---|
| United States | 20.6% | Australia | 3.8% |
| Indonesia | 8.0% | Thailand | 3.7% |
| Japan | 6.3% | Malaysia | 3.6% |
| South Korea | 5.5% | Russia / Canada / Vietnam | 각 ~3% |
02 사용자 우려 해부 — "발송시 LLM 국가판단"
우려: "수신자국 영업일로 하려면 LLM으로 국가 판단해야 하고, 수십만건이라 문제 많다." → 실데이터로 보면 세 단계 모두 LLM이 불필요:
| 단계 | 실데이터 | LLM 필요? |
|---|---|---|
| 발송시점 tz 확보 | execution.timezone 100% 저장, 잡에 동봉 | 불요 (룩업) |
| 87% 케이스 country 확보 | country 이미 기지(해외)인데 tz만 미변환 | 불요 (정적맵) |
| 잔여 미상 country | leads.country 미상 56.5% 중 ccTLD·도메인 해소 후 잔여 | 잔여만·1회·캐시 |
03 최적 아키텍처 — LLM을 발송경로에서 제거
| 계층 | 위치 | 비용 | LLM |
|---|---|---|---|
| A. 발송 게이트 | pre-check.ts, 잡의 execution.timezone | $0 (luxon 산술, μs급) | ✗ |
| B. tz 해소 cascade | enrollment 시점, 리드당 1회 | 거의 $0 | 잔여만 |
| C. 공휴일 | phase 2 데이터셋 | 1회 | ✗ |
계층 B — country→tz 우선순위 (싼 것부터, 결정론적)
- lead.country(기지) → IANA tz 정적맵 — 공짜. 10,998 오발 즉시 해소 (현재 누락, 최고 ROI)
- 이메일 ccTLD(
.co.kr·.jp·.de) → country — 공짜·결정론적 - 도메인→country Redis 캐시 — 고유 도메인 ≪ 발송건수 → 도메인당 1회 해소 후 영구 재사용
- 잔여 모호
.com→ 보수적 글로벌 디폴트 또는 배치 LLM(도메인당 1회·enrichment·영구 캐시) — LLM은 여기 한 곳, 발송경로 밖
// 발송 게이트: 잡에 이미 있는 tz로 순수 산술, LLM/DB/Redis 호출 0 const cal = businessCalendarFor(job.data.timezone) // IANA 문자열 const w = decideSendWindow(new Date(), cal) // luxon, DST 정확 if ('deferUntil' in w && cal.confident) { // 디폴트 tz면 fail-open await job.moveToDelayed(w.deferUntil.getTime()) throw new DelayedError() }
04 비용 분석 (수십만건 1배치)
| 접근 | 단가 | 수십만건 비용 | 채택 |
|---|---|---|---|
| per-send LLM (우려안) | 발송마다 호출 | 수십만 콜 ≈ $수십~수백 + 발송지연 | 회피 |
| 발송 게이트(tz 룩업+산술) | 0 | $0 | 채택 |
| country→tz 정적맵 | 0 | $0 | 채택 |
| 잔여도메인 배치 LLM(1회·캐시) | 도메인당 1회 | 수천 도메인 × 1회 ≈ 무시가능 | 선택 |
성능: 게이트는 메모리 산술 1회. BullMQ가 어차피 delayed 97k를 순회하므로 추가 부하 사실상 0. DB·Redis 추가 호출 없음(tz는 페이로드 동봉). Redis Lua/Function 불필요.
05 예상 오류 & 방어
| 오류 | 영향 | 방어 |
|---|---|---|
| tz=Seoul 디폴트(해외 87%) | 게이트가 한국기준 오판 | 출시 전 계층B부터 적용. 그 전엔 디폴트 tz fail-open(92.6% 대량보류 방지) |
| DST 드리프트 | ±1h 오발송 | 고정오프셋 TZ_OFFSET 폐기, IANA+luxon |
| 미국 6존·러시아 11존 | 시간대 뭉뚱그림 | IANA가 구체값일 때만 신뢰, 디폴트는 fail-open |
| 걸프권 금·토 주말 | 잘못 평일판정 | 어댑터 GULF set(일~목) 이미 존재 |
| TLD 오판(.io·.com) | 발송시각만 어긋남(전달성 무관) | 저위험 — 디폴트 폴백 |
| 공휴일 미반영 | 공휴일 발송 잔존 | phase 2 국가별 holiday set |
06 권장 실행 순서 (ROI순)
- (공짜·최고ROI) country→tz 변환 복구 — 기지 country 10,998 오발 즉시 해소. LLM·인프라 0.
- (공짜) ccTLD + 도메인캐시 cascade — 미상 56.5% 잠식.
- 발송 게이트 출시 —
execution.timezone+luxon, 디폴트 tz는 fail-open. - (선택·저가) 잔여 도메인 배치 LLM — enrichment 1회·영구 캐시.
- (phase 2) 국가별 공휴일.
요지
발송 때 LLM 쓰는 설계 자체가 안티패턴이고 데이터상 그럴 필요도 없다(tz 100% 보유). 가장 큰 효과는 이미 아는 country를 tz로 바꾸는 누락을 공짜로 메우는 것.
07 데이터 출처
beta postgres send-grid-test-postgres-1 (emails·sequence_step_executions·leads·email_replies, 60일/97,216건 delayed 전수) · beta redis send-grid-test-redis-1 (bull:sequence-email delayed ZSET). 모두 2026-06-11 조회.