대규모 Webhooks: 멱등성, 재생 안전, 관측 가능한 Webhook 시스템 설계

발행: (2026년 1월 20일 오전 06:19 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

소개

Webhook은 같은 결제가 세 번 처리되고, 중요한 이벤트가 누락되며, 실제로 무슨 일이 일어났는지 증명할 수 없을 때까지는 쉬워 보입니다. 이 글은 재시도, 재생, 순서가 뒤섞인 전송, 제공자 버그, 그리고 미래의 자신까지 견디는 웹훅 수집 시스템을 구축하는 생산급 심층 탐구입니다.

대부분의 웹훅 제공자는 다음을 약속합니다:

  • 최소 한 번 전달
  • 실패 시 재시도
  • 서명된 페이로드

그들이 약속하지 않는 것:

  • 순서 보장
  • 고유성
  • 일관성
  • 합리적인 재시도 동작

현실: 웹훅은 당신이 제어할 수 없는 신뢰할 수 없는 분산 큐입니다. 그렇게 취급하세요.

전형적인 실패 유형:

  • 중복 이벤트가 두 번 처리됨
  • 성공 후에도 제공자가 몇 시간 동안 재시도함
  • 이벤트가 순서대로 도착하지 않음
  • 처리 중간에 부분적인 실패
  • 시계 차이로 서명이 깨짐
  • 감사 로그 없이 조용히 누락됨

올바른 설계는 이러한 일이 매일 발생한다고 가정합니다.

아키텍처 개요

Webhook Provider

   │  POST /webhook

Ingress Layer (Fast, Stateless)

   │  enqueue

Persistent Event Store

   │  dedupe + order

Event Processor

   │  side effects

Domain Services

핵심 원칙

웹훅 핸들러에서 비즈니스 로직을 절대 수행하지 마세요.

웹훅 엔드포인트는 다음을 수행해야 합니다:

  1. 서명 검증
  2. 원시 페이로드 저장
  3. 2xx 응답 반환

그 외의 작업은 하위 단계로 넘겨야 합니다.

최소 웹훅 핸들러 (Node/Express)

app.post('/webhook', async (req, res) => {
  verifySignature(req);
  await storeRawEvent(req);
  res.status(200).end();
});

엔드포인트가 1–2초 이상 걸리면 재시도가 보장됩니다.

저장해야 할 내용

  • 모든 요청 헤더
  • 원시 요청 본문
  • 수신 타임스탬프
  • 제공자 이벤트 ID(있는 경우)

Source:

멱등성

시스템이 멱등하지 않다면, 재시도는 데이터 손상을 초래합니다.

잘못된 접근 방식

  • “이미 상태가 변경됐는지 확인하겠다” ❌
  • “제공자 이벤트 ID를 신뢰하겠다” ❌

올바른 접근 방식

자신만의 멱등성 키를 생성합니다:

const key = hash(`${provider}:${eventType}:${externalObjectId}`);

키를 고유 제약조건과 함께 영구 저장합니다. 삽입이 실패하면 중복으로 간주하고 안전하게 건너뛰세요.

순서

제공자는 순서를 보장하지 않습니다. 절대로 가정하지 마세요:

  • 이벤트 A가 이벤트 B보다 먼저 도착한다
  • 타임스탬프가 단조롭다

전략

이벤트를 상태 전이로 모델링하고 잘못된 전이는 거부합니다:

if (!isValidTransition(currentState, nextEvent)) {
  logAndIgnore();
}

이렇게 하면 유효한 상태 변화만 적용되므로 순서는 무관해집니다.

트랜잭션 아웃박스 패턴

데이터베이스는 트랜잭션을 지원하지만 외부 API는 그렇지 않습니다. 아웃박스 패턴을 사용하세요:

  1. 도메인 변경 아웃박스 레코드를 동일한 트랜잭션에 기록합니다.
  2. 트랜잭션을 커밋합니다.
  3. 비동기 워커가 대기 중인 아웃박스 레코드를 읽어 부수 효과를 실행합니다.
  4. 아웃박스 레코드를 완료된 것으로 표시합니다.

이점

  • 중복 이메일 전송, 중복 청구 및 부분 실패를 방지합니다.

Common Mistakes

  • 검증하기 전에 JSON을 파싱함.
  • 헤더 대소문자를 무시함.
  • 시스템 시계를 무조건 사용함.

Best practices

  • raw body를 기준으로 검증함.
  • 타임스탬프를 확인할 때 작은 시계 오차를 허용함.
  • 실패 시 닫힌 상태 유지: 검증에 실패하면 내부적으로 재시도하지 않음.

관찰성 및 감사

각 웹훅에 대해 세 가지 질문에 답해야 합니다:

  1. 우리가 이를 받았나요?
  2. 우리가 이를 처리했나요?
  3. 무엇이 변경되었나요?

최소 요구 사항

  • 로그 전반에 걸쳐 추적 가능한 이벤트 ID.
  • 처리 상태가 지속됨.
  • 실패에 대한 데드레터 큐.

5분 이내에 이 질문에 답할 수 없으면 시스템이 눈이 먼 것입니다. 이 중 하나라도 놓치면 되돌릴 수 없는 버그가 발생할 수 있습니다.

결론

Webhooks는 콜백이 아닙니다; 신뢰할 수 없고 재생 가능한 메시지입니다. 이렇게 취급하면—원시 페이로드를 저장하고, 멱등성을 보장하며, 순서가 뒤바뀐 전달을 처리하고, 부수 효과를 위해 아웃박스를 사용하면—지루하고 신뢰할 수 있는 인프라가 됩니다. 그리고 지루한 인프라가 목표입니다.

Back to Blog

관련 글

더 보기 »