웹훅 완벽 가이드: 서명된 페이로드와 재시도

Backend2026-11-04TryQuickToolBox

타사 결제 제공업체를 방금 통합했고, 결제가 성공하면 웹훅을 통해 앱에 알림을 보낸다고 합니다. 하지만 요청이 실제로 그 제공업체에서 온 것인지, 공격자로부터 온 것인지 어떻게 알 수 있을까요? 그리고 웹훅이 발생했을 때 서버가 잠시 다운되어 있다면 어떻게 될까요? 적절한 보호 장치 없이는 가짜 이벤트를 처리하거나 중요한 업데이트를 놓칠 위험이 있습니다. 이 글에서는 서명된 페이로드로 웹훅을 보호하고 강력한 재시도 메커니즘을 구현하는 방법을 설명합니다.

웹훅이란?

웹훅은 사용자가 정의한 HTTP 콜백입니다. 소스 시스템에서 이벤트(예: 결제 완료)가 발생하면 사용자가 제공한 URL로 HTTP POST 요청을 보냅니다. 업데이트를 반복적으로 요청하는 폴링과 달리, 웹훅은 거의 실시간으로 데이터를 푸시합니다. CI/CD 트리거, 채팅 메시지, 결제 상태 변경 등 이벤트 알림에 널리 사용됩니다.

웹훅을 서명으로 보호해야 하는 이유

웹훅 엔드포인트는 공개적으로 접근 가능하므로 누구나 POST 요청을 보낼 수 있습니다. 검증 없이는 공격자가 이벤트를 위조하여 주문을 결제 완료로 표시하는 등의 권한 없는 작업을 수행할 수 있습니다. 공유 비밀 키로 페이로드에 서명하면 진위 여부와 무결성을 확인할 수 있습니다.

서명된 페이로드의 작동 방식

발신자와 수신자는 비밀 키를 공유합니다. 각 웹훅에 대해 발신자는 요청 본문에 대한 암호화 서명(보통 HMAC)을 계산하여 헤더에 포함합니다. 수신자는 동일한 비밀 키를 사용하여 서명을 다시 계산하고 비교합니다. 일치하면 요청이 진짜입니다.

올바른 알고리즘 선택

HMAC-SHA256이 사실상 표준입니다. 빠르고 안전하며 모든 주요 언어에서 사용할 수 있습니다. 취약한 MD5나 SHA1은 피하세요.

단계별: 서명된 페이로드 검증

  1. 서명 추출 헤더(예: X-Signature)에서 서명을 추출합니다.
  2. 원시 요청 본문 가져오기 문자열 또는 바이트 배열로 가져옵니다. 먼저 파싱하지 마세요. 어떤 변환도 서명을 변경합니다.
  3. HMAC 계산 비밀 키와 원시 본문을 사용하여 HMAC을 계산합니다.
  4. 비교 계산된 서명과 수신된 서명을 상수 시간 비교를 사용하여 비교하여 타이밍 공격을 방지합니다.

다음은 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 hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(req.body).digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) {
    return res.status(401).send('Invalid signature');
  }

  // 이벤트 처리
  const event = JSON.parse(req.body);
  // ... 이벤트 처리

  res.status(200).send('OK');
});

흔한 함정: 검증 전에 JSON 본문 파서를 사용하는 것입니다. Express의 express.json()은 본문을 파싱하므로 원시 바이트가 손실됩니다. 웹훅 라우트에는 항상 express.raw()를 사용하거나 원시 본문을 별도로 캡처하세요.

재시도 전략 설계

서명이 있어도 웹훅은 실패할 수 있습니다. 서버가 다운되었거나 요청이 시간 초과될 수 있습니다. 신뢰할 수 있는 발신자는 실패한 전송을 재시도합니다. 수신자로서 재시도를 우아하게 처리해야 합니다.

발신자 측 재시도

발신자를 구축하는 경우 지터를 포함한 지수 백오프를 구현하세요. 예를 들어 1분 후, 5분 후, 30분 후에 재시도하고 최대 시도 횟수(예: 5회)까지 수행합니다. 수신자가 중복 제거할 수 있도록 페이로드에 고유 이벤트 ID를 포함하세요.

수신자 측 멱등성

재시도로 인해 중복 전송이 발생할 수 있으므로 엔드포인트는 멱등성을 가져야 합니다. 이벤트 ID를 사용하여 처리된 이벤트를 추적하세요. 고유 제약 조건이 있는 데이터베이스에 저장하거나 TTL이 있는 Redis와 같은 캐시를 사용하세요. 이벤트 ID가 이미 본 것이라면 즉시 200 OK를 반환하고 재처리하지 마세요.

데이터베이스 고유 인덱스를 사용한 예:

async function handleWebhook(event) {
  try {
    await db.query('INSERT INTO processed_events (event_id) VALUES ($1)', [event.id]);
  } catch (err) {
    if (err.code === '23505') { // 고유 위반
      return; // 이미 처리됨
    }
    throw err;
  }
  // 이벤트 처리
}

웹훅 엔드포인트 모범 사례

비교: 서명된 웹훅 vs 서명되지 않은 웹훅

측면 서명된 웹훅 서명되지 않은 웹훅
보안 높음 – 발신자와 무결성 검증 낮음 – 누구나 요청 위조 가능
구현 복잡성 보통 – HMAC 및 비밀 관리 필요 낮음 – POST만 수락
사용 사례 결제, 민감한 데이터 중요하지 않은 알림

FAQ

Python에서 웹훅 서명을 어떻게 검증하나요?

hmac 모듈과 SHA256을 사용하세요. 계산된 다이제스트를 헤더와 비교할 때 상수 시간 비교를 위해 hmac.compare_digest를 사용하세요.

웹훅 엔드포인트가 중복 이벤트를 받으면 어떻게 해야 하나요?

이벤트 ID를 처리된 ID 저장소와 대조하세요. 중복이면 200 OK를 반환하고 처리를 건너뛰세요. 이렇게 하면 멱등성이 보장됩니다.

웹훅 발신자는 몇 번 재시도해야 하나요?

정해진 횟수는 없지만 지수 백오프(예: 1분, 5분, 30분)를 사용하여 3~5회 재시도하는 것이 일반적입니다. 그 후에는 전송 실패로 표시하고 알림을 보내세요.

웹훅 엔드포인트를 테스트할 준비가 되셨나요? 개발 중에 페이로드를 예쁘게 출력하고 유효성을 검사하려면 JSON Formatter를 사용하세요.