ADR-0022: Payment Time Jitter¶
정기 결제 시점에 ±15분의 랜덤 jitter 를 적용하여 같은 시각 결제 폭주를 분산한다.
Status¶
Accepted
- Decided at — 2026-04-13
- Decided by — Pablo
Context¶
Umbra 의 구독은 정각 결제 정책(current_period_end 도달 시점에 결제 시도)을 채택했다. 그러나 현실에서는 다음 문제가 발생한다.
- 대부분 사용자가 매월 1일 0시 또는 월초 유사한 시각 에 가입 → 결제 만료 시점이 몰림
- 수천 개 구독이 같은 초에 일제히 결제 → Toss API rate limit 접근, worker pool 경합
- 한국 카드사의 야간 점검 시간(23:30~01:00)에 결제 만료가 몰리면 대량 실패 위험
즉 "정각 결제" 는 UX 가 깔끔하지만 시스템 부하 측면에서 문제.
Decision¶
정기 결제 시점에 ±15분 랜덤 jitter 를 적용한다.
- 구독 생성 시 또는 첫 결제 성공 시
next_billing_at = current_period_end + random(-15min, +15min)설정 - 이후 결제는 jitter 된 시각을 기준으로 반복
- jitter offset 은 subscription 단위로 고정 (매 사이클마다 재랜덤 안 함) 으로 사용자 경험 일관성 유지
구현:
offset := time.Duration(rand.Intn(30*60)-15*60) * time.Second
nextBillingAt := currentPeriodEnd.Add(offset)
선택 근거:
- 결제 폭주 분산 — ±15분 범위에 균등 분포되면 같은 시각 결제 요청이 1/30 수준으로 희석
- 사용자 체감 영향 0 — 15분 차이는 사용자 인지 불가
- 카드사 점검 시간 회피 — 23:30 정각에 만료되는 구독도 jitter 로 23:15~23:45 에 분산
- 단순 구현 — 별도 스케줄러 설계 없이 cron + jitter 로 해결
Consequences¶
Positive¶
- Worker pool 및 Toss API 호출이 시간축에 균등 분산
- 카드사 야간 점검으로 인한 대량 실패 위험 감소
- 사용자 영향 0 (15분은 결제 날짜 개념에서 무의미)
- 결제 로그/메트릭 시각화 시 spike 완화
Negative¶
- "매월 1일 0시" 같은 정확한 시각을 약속할 수 없음 (마케팅 또는 UX 에 작은 영향)
- 구독별로 결제 시점이 미세하게 달라 디버깅 시 정확한 시각 예측 어려움
- 사용자가 "왜 내 결제가 정각이 아니지" 물으면 설명 필요 (FAQ 수준)
Neutral¶
- jitter 범위(±15분)는 운영 데이터 기반으로 조정 가능
- 같은 user 의 여러 구독은 독립적으로 jitter 적용 → 같은 User 도 결제가 분산됨
Decision details¶
Jitter lifecycle¶
- 구독 생성 시 —
next_billing_at계산에 jitter 포함 - 결제 성공 시 — 다음
current_period_end계산 후 같은 offset 유지 (매월 같은 분에 결제) - 재시도 시 — jitter 무관, 정책에 따라 24h/48h/72h 후 재시도
Jitter range¶
- 범위: ±15분 (총 30분 구간)
- 분포: 균등 랜덤 (uniform)
- 근거: 한국 카드사 점검 창(보통 30분~1시간)을 회피하기에 충분하면서 사용자 인지 불가 수준
Storage¶
billing.subscriptions.next_billing_at(TIMESTAMPTZ) 컬럼에 jitter 적용된 시각 저장- 별도 offset 컬럼은 두지 않음 (필요 시 다음 주기 계산에서 재사용)
Alternatives considered¶
Alternative 1: 정각 결제 유지 (jitter 없음)¶
Pros
- 사용자에게 명확한 결제 시각 약속 가능
- 구현 단순
Cons
- 결제 폭주로 인한 Toss rate limit 우려
- 카드사 점검 시간과 충돌 위험
- MVP 에서도 큰 문제는 아니지만 규모 확대 시 명백한 병목
Why rejected — 사용자 영향은 0 인데 시스템 안전성 이점이 큼.
Alternative 2: ±1시간 jitter¶
Pros
- 분산 효과 더 강함
Cons
- 사용자에게 "결제 시각 오차 1시간" 은 인지 가능한 범위
- 마케팅 시 "매월 1일 결제" 가 날짜 경계를 넘길 수 있음 (23:30 가입 시 익일로 넘어감)
Why rejected — ±15분이 분산과 UX 균형에서 적정.
Alternative 3: 시간대 기반 분산 (12구간)¶
Pros
- 특정 시간대 부하 0 보장
Cons
- 구현 복잡
- jitter 대비 추가 이점 적음
Why rejected — 과도한 복잡도.
Alternative 4: Worker 측에서 backoff + retry¶
Pros
- 결제 시점은 정각 유지, 처리 과정에서만 분산
Cons
- Toss rate limit hit 는 그대로 발생
- Worker queue 에 동일 시점 수천 건 쌓임 → 메모리 부담
Why rejected — 근본 원인 해결이 아님.
Compliance¶
billing.subscriptions.next_billing_at은 jitter 가 적용된 값을 저장- 새 구독 생성 로직은 반드시
platform/billing/jitter.go(또는 동등 헬퍼) 를 경유 - 정각 기반 assertion 을 테스트에 쓰지 않음 (flaky test 방지)
- UI 는 "다음 결제일" 만 날짜 기준으로 표시 (분 단위 표기 지양)
Revisit triggers¶
- Toss rate limit 이 급증하여 ±15분으로 부족하면 범위 확대 (±30분 등)
- 구독 수가 커지면 jitter 범위와 worker pool 크기 연동 최적화
- 특정 시간대 카드사 점검이 불규칙해지면 deny-list 방식 (해당 시간대는 피해서 스케줄) 추가 검토