2026-08-09
던닝(Dunning) 재시도 주기, 왜 1일·3일·5일인가
정기결제 시스템에서 결제 실패는 예외가 아니라 일상입니다. 카드가 만료됐거나, 한도가 초과됐거나, 일시적으로 은행 시스템이 응답하지 않을 수도 있습니다. 이때 곧바로 구독을 끊어버리면 살릴 수 있었던 매출을 잃습니다. 결제 실패 후 언제, 몇 번 재시도할지 정하는 정책을 던닝(Dunning)이라 부릅니다. 이 글은 슈퍼빌링이 실제로 채택한 1일·3일·5일 재시도 스케줄을 예시로, 던닝 주기를 설계할 때 고려해야 할 것들을 정리합니다.
비자발적 이탈(Involuntary Churn)이라는 문제
구독을 스스로 해지하는 게 아니라, 결제가 실패해서 자동으로 서비스가 끊기는 걸 비자발적 이탈이라고 합니다. Chargebee, Baremetrics 같은 해외 구독결제 인프라 업체들은 SaaS 매출 손실의 상당 부분이 이 비자발적 이탈에서 발생한다고 공통적으로 지적합니다. 카드 한도 초과나 일시적 은행 오류는 며칠 안에 해결되는 경우가 많기 때문에, 즉시 해지 대신 재시도 기간을 두는 것만으로 되살릴 수 있는 매출이 상당합니다.
슈퍼빌링의 재시도 스케줄: 1일 → 3일 → 5일
// 던닝 재시도 간격(일). N번째 실패 후 이 인덱스만큼 뒤로 다음 청구를 미룬다.
// 3회 재시도(dunningAttempt가 이 배열 길이에 도달)까지 모두 실패하면
// 구독을 past_due로 전환하고 재시도를 멈춘다.
const DUNNING_RETRY_DAYS = [1, 3, 5];최초 청구가 실패하면 1일 뒤 재시도하고, 그것도 실패하면 3일 뒤, 그마저 실패하면 5일 뒤 마지막으로 시도합니다. 세 번 모두 실패하면 구독을 past_due(연체) 상태로 전환하고 재시도를 멈춥니다.
이 간격을 점점 늘려가는 이유는 두 가지입니다.
- 초반은 짧게: 결제 실패의 상당수는 일시적 오류(네트워크 문제, PG 응답 지연)라 하루 안에 저절로 해결되는 경우가 많습니다. 첫 재시도를 너무 늦추면 회수 가능한 매출을 놓칩니다.
- 후반은 길게: 카드 한도 초과나 만료처럼 사용자가 직접 조치해야 하는 문제는 며칠 걸립니다. 간격을 벌려주는 대신 시도 횟수를 아끼는 게 합리적입니다.
실제 처리 흐름
매일 실행되는 청구 배치(chargeDueSubscriptions)가 결제일이 도래한 구독을 순회하며 청구를 시도합니다. 실패하면 dunningAttempt를 1 올리고, DUNNING_RETRY_DAYS 배열에서 그에 해당하는 일수만큼 뒤로 nextBillingAt을 미룹니다.
const attempt = row.subscription.dunningAttempt + 1;
if (attempt > DUNNING_RETRY_DAYS.length) {
// 재시도를 모두 소진했다 — past_due로 전환
await db.update(subscriptions)
.set({ state: "past_due", dunningAttempt: 0 })
.where(eq(subscriptions.id, row.subscription.id));
} else {
const retryDays = DUNNING_RETRY_DAYS[attempt - 1];
const nextRetryAt = new Date(Date.now() + retryDays * 24 * 60 * 60 * 1000);
await db.update(subscriptions)
.set({ nextBillingAt: nextRetryAt, dunningAttempt: attempt })
.where(eq(subscriptions.id, row.subscription.id));
}성공하면 dunningAttempt를 0으로 초기화하고 다음 정기 결제일로 넘어갑니다. 재시도 여부와 무관하게 매 시도마다 invoice.payment_succeeded 또는 invoice.payment_failed 웹훅을 발송해, 고객사가 실패 시점마다 사용자에게 알림을 보낼 수 있게 합니다.
직접 설계한다면 고려할 것들
- 재시도 횟수와 총 유예 기간의 트레이드오프: 재시도를 늘릴수록 회수 가능성은 높아지지만, 그만큼 매출이 없는 상태로 서비스를 제공하는 기간도 늘어납니다. 슈퍼빌링은 3회·총 9일(1+3+5)로 균형을 잡았습니다.
- 간격을 점증시킬지 고정할지: 매번 같은 간격(예: 3일마다)으로 재시도할 수도 있지만, 초반엔 짧게 후반엔 길게 가는 쪽이 일시적 오류는 빨리 잡고 근본적 문제는 사용자에게 대응 시간을 준다는 점에서 더 합리적입니다.
- 재시도 소진 후 처리: 단순히 구독을 끊는 대신
past_due같은 중간 상태를 둬서, 사용자가 나중에 결제수단을 갱신하면 다시 활성화할 수 있는 여지를 남기는 걸 권장합니다. - 알림과의 연동: 재시도만으로는 부족합니다. 카드 만료 임박이나 결제 실패 시 이메일 알림을 함께 보내야 사용자가 능동적으로 결제수단을 갱신할 확률이 올라갑니다.
정리
- 결제 실패를 즉시 해지로 처리하지 말고, 재시도 기간을 둬서 비자발적 이탈을 줄이세요.
- 재시도 간격은 초반엔 짧게(일시적 오류 대응), 후반엔 길게(사용자 조치 시간 확보) 설계하는 걸 권장합니다.
- 슈퍼빌링은 이 던닝 로직을 포함해 정기 청구 스케줄러 전체를 이미 구현해뒀습니다. Quickstart 문서에서 3분 안에 연동해볼 수 있습니다.