웹훅 설명: 서명된 페이로드와 재시도

Backend2026-10-17TryQuickToolBox

결제 게이트웨이를 방금 통합했습니다. 문서에는 "결제가 성공하면 엔드포인트로 웹훅을 보내겠습니다"라고 나와 있습니다. 간단한 라우트를 설정하고 로컬에서 테스트한 후 배포합니다. 그런데 프로덕션에서 중복 이벤트, 누락된 알림, 그리고 원인 모를 500 오류가 발생합니다. 웹훅은 단순해 보이지만, 그렇지 않을 때가 있습니다.

이 가이드에서는 웹훅이 실제로 어떻게 작동하는지, 서명된 페이로드가 왜 중요한지, 그리고 문제가 발생해도 시스템을 동기화 상태로 유지하는 재시도 로직을 구축하는 방법을 설명합니다.

웹훅이란 무엇인가?

웹훅은 HTTP 콜백입니다. 특정 이벤트가 발생하면 한 시스템이 사용자가 제공한 URL로 HTTP POST 요청을 보냅니다. "무슨 일이 있었나요?"라고 반복적으로 묻는 폴링과 달리, 웹훅은 거의 실시간으로 이벤트를 푸시합니다.

일반적인 사용 사례는 다음과 같습니다:

발신자(예: Stripe, GitHub)는 웹훅 제공자이고, 귀하의 서버는 웹훅 소비자입니다.

서명된 페이로드가 필수적인 이유

웹훅 엔드포인트는 공개적으로 접근 가능합니다. URL을 아는 사람은 누구나 가짜 이벤트를 보낼 수 있습니다. 검증이 없으면 공격자가 "결제 성공" 이벤트를 위조하여 상품을 무료로 배송할 수 있습니다.

서명된 페이로드는 귀하와 제공자만 알고 있는 공유 비밀로 생성된 암호화 서명을 요청 헤더에 포함하여 이 문제를 해결합니다. 웹훅을 받으면 서명을 다시 계산하여 비교합니다. 일치하면 페이로드가 진짜입니다.

HMAC 서명은 어떻게 작동하나요?

대부분의 제공자는 HMAC-SHA256을 사용합니다. 제공자는 다음과 같이 계산합니다:

signature = HMAC-SHA256(secret, payload)

그리고 X-Signature와 같은 헤더에 넣어 보냅니다. 귀하의 서버도 동일한 계산을 수행하고 일치 여부를 확인합니다. 비밀은 비밀이므로 제공자만 유효한 서명을 생성할 수 있습니다.

실제로 서명 검증하기

Express를 사용한 최소한의 Node.js 예제입니다:

const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-signature'];
  const secret = process.env.WEBHOOK_SECRET;
  const payload = req.body; // raw Buffer

  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send('Invalid signature');
  }

  // Process the event
  const event = JSON.parse(payload);
  console.log('Received event:', event.type);
  res.status(200).send('OK');
});

app.listen(3000);

중요: 서명 검증에는 항상 원시 요청 본문을 사용하세요. JSON을 먼저 파싱하면 직렬화된 문자열이 달라질 수 있어(공백, 키 순서) 서명 검사가 실패합니다.

흔한 함정

재시도: 실패를 우아하게 처리하기

네트워크는 실패하고, 서버는 재시작되며, 배포는 발생합니다. 웹훅 제공자는 엔드포인트가 일시적으로 사용할 수 없을 수 있다고 예상하므로 실패한 전달을 재시도합니다. 그러나 재시도는 새로운 문제를 야기합니다: 중복 이벤트, 순서가 뒤바뀐 전달, 그리고 주의하지 않으면 무한 루프입니다.

제공자가 재시도하는 방법

대부분의 제공자는 지수 백오프를 사용합니다: 첫 실패 후 짧게 기다린 다음 점진적으로 간격을 늘립니다. 예를 들어 Stripe는 최대 3일 동안 지연을 늘려가며 재시도합니다. GitHub는 최대 8회 재시도합니다. 정확한 일정은 다르지만 패턴은 일관됩니다.

엔드포인트는 수신을 확인하기 위해 빠르게 2xx 상태 코드를 반환해야 합니다. 다른 상태(4xx, 5xx)는 실패를 알리고 재시도를 트리거합니다.

멱등 핸들러 구축하기

재시도로 동일한 이벤트가 여러 번 전달될 수 있으므로 핸들러는 멱등이어야 합니다: 동일한 이벤트를 두 번 처리해도 한 번 처리한 것과 같은 효과를 가져야 합니다.

일반적인 전략:

간단한 인메모리 집합을 사용한 예(설명용; 프로덕션에서는 영구 저장소 사용):

const processedEvents = new Set();

app.post('/webhook', (req, res) => {
  const event = req.body;
  if (processedEvents.has(event.id)) {
    return res.status(200).send('Already processed');
  }
  // Process event...
  processedEvents.add(event.id);
  res.status(200).send('OK');
});

빠르게 응답하기

제공자는 종종 타임아웃(예: 5초)을 가집니다. 핸들러가 무거운 처리를 수행하면 즉시 확인하고 비동기적으로 처리하세요. 예를 들어 이벤트를 큐에 푸시하고 200을 반환합니다.

안정적인 웹훅을 위한 모범 사례

  1. 항상 서명을 검증하세요 — 원시 본문과 상수 시간 비교를 사용합니다.
  2. HTTPS를 사용하세요 — 도청과 변조를 방지하기 위해 엔드포인트에 HTTPS를 사용합니다.
  3. 멱등성을 구현하세요 — 중복 전달을 처리합니다.
  4. 모든 것을 로깅하세요 — 수신 페이로드, 서명, 응답을 디버깅을 위해 기록합니다.
  5. 실패를 모니터링하세요 — 반복되는 오류에 대한 알림을 설정합니다.
  6. 실제 이벤트로 테스트하세요 — 제공자 대시보드나 CLI 도구를 사용합니다.
  7. 순서가 뒤바뀐 이벤트를 처리하세요 — 로직에 타임스탬프나 시퀀스 번호를 포함합니다.

비교: 폴링 vs. 웹훅

측면폴링웹훅
지연 시간간격에 따라 다름거의 실시간
서버 부하높음(지속적인 요청)낮음(이벤트 발생 시에만)
복잡성구현이 간단함엔드포인트, 보안, 재시도 필요
안정성재시도를 직접 제어제공자가 재시도 제어

FAQ

웹훅 서명이란 무엇인가요?

웹훅 서명은 공유 비밀로 생성된 페이로드의 암호화 해시(보통 HMAC-SHA256)입니다. 수신자가 요청이 예상 발신자로부터 왔고 변조되지 않았음을 확인할 수 있게 합니다.

중복 웹훅 이벤트를 어떻게 처리하나요?

이벤트 ID를 추적하거나 자연스럽게 멱등인 데이터베이스 작업을 사용하여 핸들러를 멱등하게 만드세요. 변경 사항을 적용하기 전에 이벤트가 이미 처리되었는지 확인하세요.

웹훅 엔드포인트는 무엇을 반환해야 하나요?

이벤트를 수신하고 검증한 즉시 2xx 상태 코드(예: 200 OK)를 반환하세요. 무거운 처리가 필요하면 즉시 확인하고 비동기적으로 작업을 처리하세요.

웹훅 페이로드를 테스트할 준비가 되셨나요? JSON Formatter를 사용하여 수신 이벤트의 JSON 구조를 검사하고 검증하세요.