국가별 영업일 발송 게이트
2026 기준 최적성 재분석
발송 시점에 한국기준 주말이면 영업일로 미루는 기능 — Redis 스크립트 가능성 · 헥사고날 적합성 · 2026 베스트프랙티스 정합성
00 결론 (TL;DR)
✅ 최적
앞서 제안한 "순수 TS 도메인 규칙 + BullMQ native
moveToDelayed 재지연" 안은 2026 기준에서도 최적이다. 웹 그라운딩 결과 오히려 권고가 강화됨.
⚠️ 정정
주말 판정을 위한 전용 Redis Lua/EVAL 스크립트는 2026 기준 안티패턴. ① 공유 가변상태가 없는 무상태 연산이라 Redis가 불필요하고 ② 굳이 서버사이드 로직을 Redis에 둔다면 7.0+에서는 EVAL이 아니라 Redis Functions가 권장이기 때문. → Redis 스크립트는 throttle slot과의 atomic 융합이 꼭 필요할 때만 정당화된다.
| 질문 | 답 | 핵심 근거 |
|---|---|---|
| Redis 스크립트로 발송시 주말 차단 가능? | 가능 | KST=UTC+9 고정(서머타임 없음) → 정수연산만으로 요일 판정, 마이크로초급. 단 Redis는 선택사항. |
| 헥사고날 구현 가능? | 이미 깔림 | domain.ts(순수) + ooo-resend.service.ts(포트 주입) 패턴 존재 → 케이스만 추가. |
| 2026 최적? | 조건부 | 규칙=순수 TS, 지연=BullMQ native, UTC 변환은 자명(KST 고정). Redis는 atomic 필요시만, 그때도 Functions. |
01 발송 경로와 삽입 지점
발송 워커 오케스트레이터(sequence-email-worker/processor.ts:46)는 claim 이전에 runPreChecks()를 호출한다. 여기서 throttle·daily-limit이 이미 job.moveToDelayed(...) + throw DelayedError() 로 재지연하는 패턴이 있어, 주말 게이트를 같은 자리에 한 줄로 얹으면 된다.
PrerunPreChecks — concurrency·throttle·daily-limit+ 주말 게이트
0validateAndClaim — execution claim
1resolveLead + bounce check
2verifyEmail + dedup
3resolveContent
4+5send + status update
claim 전에 미루므로 execution이 "processing"으로 박혀 deadlock 되는 일이 없다(이 파일 주석이 명시한 제약과 일치). throttle의 슬롯거부 재지연(pre-check.ts:73)과 완전히 동일한 메커니즘이라 신규 인프라 0.
02 2026 베스트프랙티스 정합성 검증
Redis: EVAL 스크립트 → Functions, 그러나 여기선 둘 다 불요
- Redis 공식 문서·2026 가이드 기준 신규 서버사이드 로직은 Redis Functions가 권장. EVAL은 무명·비영속·CI/CD 통합 불가라 "클라이언트 앱의 일부"로 취급되어 하위호환용으로만 유지.
- 현행
RESERVE_NEXT_SLOT_LUA(sequence-email-scheduler.ts:91)는 legacy EVAL 패턴. throttle 카운터라는 공유 가변상태에 대한 atomic read-modify-write라 Redis에 둘 정당성은 있음. 하지만 새로 손댄다면 Functions로 이관이 정석. - 주말 판정엔 공유상태가 없다. 순수 시간함수이므로 EVAL이든 Functions든 Redis에 둘 이유 자체가 없다 → 워커 메모리에서 계산.
BullMQ: UTC 변환 + native delayed job
- 2026 가이드: BullMQ는 타임존을 모름 → 모든 계산을 UTC로 변환 후 delay. KST는 고정 +9(서머타임 없음)이라 변환이 자명 —
date-fns-tz같은 라이브러리도 불필요. - 먼 미래 스케줄 회피 권고 → 주말 deferral은 최대 ~3일(금요일 저녁 → 월요일 09:00)이라 안전.
- 취소·재지연을 위한 job id 설계, 시간주입 테스트(sinon/jest) 권고 → 본 설계의
clock포트 주입과 정확히 일치.
03 헥사고날 설계 (기존 자산 재사용)
| 레이어 | 기존 자산 | 주말 게이트 적용 |
|---|---|---|
| 도메인(순수) | modules/email-automation/domain.ts — isBusinessOpen(), nextBusinessSlot() | 그대로 재사용. workDays=[1..5]면 주말 자동 제외 |
| 어댑터(캘린더) | business-calendar.adapter.ts — businessCalendarFor() | KST 캘린더 1개 |
| 포트 주입 | ooo-resend.service.ts:103 — { calendar, clock, scheduler } | 동일 시그니처로 send-time 게이트 |
| 드라이빙 어댑터 | — | scheduler.reschedule = ms => job.moveToDelayed(ms) |
핵심 트레이드오프 (Karpathy 지침: 단순성·명시)
⚠️ 상충
영업일 규칙을 Lua 문자열 안에 넣으면 stringly-typed·단위테스트 불가가 되어 헥사고날의 핵심 이점을 스스로 깬다. 규칙은 TS 순수 도메인, Lua/Function은 "clamp 산술"만 하는 멍청한 어댑터로 한정해야 둘 다 만족.
// 도메인: 순수함수, DB·Redis 없이 단위테스트 export function decideSendWindow(now: Date, cal: BusinessCalendar) : { sendNow: true } | { deferUntil: Date } { if (isBusinessOpen(now, cal)) return { sendNow: true } return { deferUntil: nextBusinessSlot(now, cal) } } // pre-check 어댑터: 기존 throttle 재지연과 동일 패턴 const w = decideSendWindow(new Date(), KST_CAL) if ('deferUntil' in w) { await job.moveToDelayed(w.deferUntil.getTime()) throw new DelayedError() }
04 권장 구현안 (우선순위)
- 규칙 = 순수 도메인 —
decideSendWindow()를domain.ts에. clock/scheduler mock으로 단위테스트(ooo-resend 방식 그대로). - 발송시점 게이트 =
pre-check.ts— Redis 불요. 가장 단순하고 2026 BullMQ 권고(native delay)와 정합. 1순위 권장 - atomic이 꼭 필요하면 — producer의
RESERVE_NEXT_SLOT_LUA에 주말 clamp 융합. 단 신규 작성이면 EVAL 대신 Redis Function으로. 조건부 - 전용 주말 Lua 스크립트 신설 — 비권장 무상태 로직의 오버엔지니어링, 헥사고날·2026 양쪽에 역행.