콘텐츠로 이동

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 방식 (해당 시간대는 피해서 스케줄) 추가 검토

References

  • ADR-0013 — Toss Billing (결제 실행 흐름)
  • ADR-0015 — asynq cron 이 결제 트리거