2026-08-10

웹훅 재시도와 멱등성(Idempotency), 중복 처리를 막는 법

웹훅은 네트워크 위에서 동작하는 이상 실패를 전제로 설계해야 합니다. 고객사 서버가 순간적으로 다운되거나, 타임아웃이 나거나, 502를 반환하는 상황은 항상 일어납니다. 그래서 웹훅 발신자는 재시도를 해야 하고, 재시도를 하는 순간 "같은 이벤트를 두 번 받는" 문제가 따라옵니다.

재시도는 몇 번, 어떤 간격으로

무한 재시도는 리소스 낭비이고, 재시도가 없으면 일시적 장애만으로도 중요한 이벤트(결제 성공, 구독 해지)를 영영 놓칠 수 있습니다. 슈퍼빌링은 웹훅 발송 실패 시 3회까지 재시도하며, 매번 간격을 늘리는 지수 백오프(exponential backoff)를 사용합니다.

1차 시도 실패 → 1분 후 재시도
2차 시도 실패 → 5분 후 재시도
3차 시도 실패 → 30분 후 재시도, 이후 포기하고 실패로 기록

3회 모두 실패하면 대시보드에 "웹훅 발송 실패" 로그를 남겨, 고객사가 수동으로 재발송을 요청하거나 자체 조회 API로 상태를 다시 가져갈 수 있게 합니다.

서명 검증만으로는 중복을 못 막는다

웹훅 서명 검증(HMAC-SHA256) 구현하는 법에서 다룬 서명 검증은 "이 요청이 진짜 우리가 보낸 것인가"를 확인할 뿐, "이 이벤트를 이미 처리했는가"는 알려주지 않습니다. 재시도로 같은 이벤트가 두 번 오면, 서명은 둘 다 유효합니다.

이벤트 ID 기반 멱등 처리

해결책은 모든 웹훅 페이로드에 고유한 이벤트 ID(event_id)를 포함시키고, 수신 측에서 이미 처리한 이벤트 ID를 저장해두는 것입니다.

app.post("/webhooks/superbilling", async (req, res) => {
  const event = req.body;

  const alreadyProcessed = await db.webhookEvents.findOne({ eventId: event.id });
  if (alreadyProcessed) {
    return res.status(200).send("already processed"); // 200으로 응답해 재시도 중단
  }

  await db.webhookEvents.insertOne({ eventId: event.id, processedAt: new Date() });
  await handleEvent(event); // 실제 비즈니스 로직

  res.status(200).send("ok");
});

여기서 중요한 것은 "이미 처리했다"는 기록과 "실제 처리 로직"을 하나의 트랜잭션처럼 묶어야 한다는 점입니다. 둘을 분리하면, 기록만 하고 처리 로직이 실패했을 때 그 이벤트를 영영 처리 못 하게 되는 반대쪽 버그가 생깁니다.

순서 보장은 하지 않는다

웹훅은 재시도 지연 때문에 발송 순서와 도착 순서가 달라질 수 있습니다. 예를 들어 subscription.created 재시도가 늦어지는 동안 invoice.payment_succeeded가 먼저 도착하는 경우입니다. 순서에 의존하지 않으려면, 각 이벤트 페이로드에 리소스의 최신 상태를 전부 포함시켜서 수신 측이 "지금 이 리소스가 어떤 상태인지"를 이벤트 하나만으로 판단할 수 있게 하는 것이 안전합니다. 이벤트 설계 전반은 정기결제 웹훅, 어떤 이벤트를 설계해야 할까를 참고하세요.

더 넓은 구독결제 연동 흐름은 구독결제 연동 완전 가이드에서 확인할 수 있습니다.

3분만에 구독결제 연동해보기

요금제 설계부터 웹훅 등록까지, 슈퍼빌링으로 바로 시작할 수 있습니다.

무료로 시작하기

이메일만 입력하면 바로 가입·로그인이 끝나요