웹훅 완벽 가이드: 서명된 페이로드와 재시도
방금 결제 게이트웨이를 통합했습니다. 문서에는 이렇게 적혀 있죠. "결제가 성공하면 저희가 귀하의 엔드포인트로 POST 요청을 보냅니다." 간단하죠? JSON을 받아서 데이터베이스를 업데이트하고 200을 반환하는 간단한 라우트를 만들면 됩니다. 그런데 문제가 생깁니다. 고객의 결제는 성공했지만 서버가 잠시 다운되어 웹훅이 도착하지 않습니다. 더 심한 경우에는 두 번 도착해서 계정에 이중으로 크레딧이 들어갑니다. 웹훅은 실패하기 전까지는 단순해 보입니다.
이 글에서는 웹훅이 실제로 어떻게 동작하는지, 서명된 페이로드가 보안에 왜 필수적인지, 그리고 문제가 발생해도 통합을 안정적으로 유지하는 재시도 로직을 구축하는 방법을 설명합니다.
웹훅이란 무엇인가?
웹훅은 HTTP 콜백입니다. 애플리케이션이 API를 폴링해서 변경 사항을 확인하는 대신, 이벤트가 발생하면 공급자가 지정한 URL로 HTTP POST를 보냅니다. 풀이 아니라 푸시인 셈이죠.
대표적인 예로는 결제 확인(Stripe, PayPal), CI/CD 빌드 알림(GitHub, GitLab), 메시징 플랫폼(Slack, Discord) 등이 있습니다. 패턴은 단순합니다. URL을 등록하면 공급자가 해당 URL로 JSON 페이로드를 보내고, 서버가 이를 처리합니다.
하지만 이 단순함 속에는 두 가지 중요한 과제가 숨어 있습니다. 진위성(요청이 정말 공급자로부터 온 것인지 어떻게 아는가?)과 신뢰성(엔드포인트가 다운되거나 느리면 어떻게 되는가?)입니다.
서명된 페이로드가 중요한 이유
웹훅 엔드포인트는 공개 URL입니다. 누구나 POST 요청을 보낼 수 있죠. 검증이 없으면 공격자가 결제 성공 이벤트를 위조하거나, 배송을 트리거하거나, 데이터를 손상시킬 수 있습니다.
서명된 페이로드는 요청 헤더에 암호화 서명을 포함시켜 이 문제를 해결합니다. 공급자는 공유 비밀 키와 요청 본문을 사용해 HMAC(해시 기반 메시지 인증 코드)을 계산한 뒤 페이로드와 함께 전송합니다. 서버는 HMAC을 다시 계산해 비교합니다. 일치하면 요청은 진짜입니다.
HMAC 서명은 어떻게 동작하는가
- 공급자와 사용자는 비밀 키를 공유합니다(설정 시 제공됨).
- 각 웹훅마다 공급자는 타임스탬프와 원본 요청 본문을 연결한 뒤 비밀 키로 HMAC-SHA256 해시를 계산합니다.
- 서명은 보통
X-Signature또는Stripe-Signature같은 헤더로 전송됩니다. - 서버는 동일한 비밀 키로 HMAC을 다시 계산해 수신한 서명과 비교합니다.
- 일치하면 이벤트를 처리하고, 그렇지 않으면 401로 거부합니다.
HMAC 계산에는 반드시 원본 요청 본문을 사용하세요. 검증 전에 프레임워크가 JSON을 파싱하면 본문이 변경될 수 있어(공백, 키 순서) 검증이 실패할 수 있습니다.
예제: 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; // Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// 이벤트 처리
const event = JSON.parse(payload.toString());
// ... 이벤트 처리
res.status(200).send('OK');
});
타이밍 공격을 방지하기 위해 timingSafeEqual을 사용한 점에 주목하세요. 서명 비교에 단순한 ===를 절대 사용하지 마세요.
재시도 로직 설계하기
서명이 있어도 웹훅은 실패할 수 있습니다. 서버가 다운되거나, 네트워크가 요청을 드롭하거나, 처리 시간이 초과될 수 있죠. 공급자는 보통 실패한 전달을 재시도하지만, 여러분의 시스템도 재시도를 우아하게 처리하도록 설계해야 합니다.
무엇이 실패로 간주되는가?
대부분의 공급자는 2xx가 아닌 모든 응답을 실패로 간주합니다. 여기에는 3xx 리다이렉트, 4xx 클라이언트 오류, 5xx 서버 오류가 포함됩니다. 일부는 타임아웃(예: 5초)도 적용합니다. 빠르게 응답하지 않으면 전달이 실패로 표시되고 재시도됩니다.
지터를 포함한 지수 백오프
공급자는 보통 지수 백오프로 재시도합니다. 첫 재시도는 1분 후, 그다음 5분, 30분 등입니다. 지터(무작위 변동)를 추가하면 많은 웹훅이 동시에 실패할 때 발생하는 썬더링 허드를 방지할 수 있습니다.
웹훅 발신자를 구축한다면 다음과 같이 백오프를 구현하세요.
- 재시도 1: 10초 후
- 재시도 2: 1분 후
- 재시도 3: 5분 후
- 재시도 4: 30분 후
- 재시도 5: 2시간 후
최대 시도 횟수를 초과하면 이벤트를 수동 검사를 위해 데드레터 큐로 이동시키세요.
멱등성: 중복 전달 처리하기
재시도는 엔드포인트가 동일한 이벤트를 여러 번 받을 수 있다는 뜻입니다. 처리는 멱등해야 합니다. 즉, 같은 이벤트를 두 번 처리해도 한 번 처리한 것과 같은 결과여야 합니다.
일반적인 전략:
- 이벤트 ID 중복 제거: 처리된 이벤트 ID를 데이터베이스나 캐시에 저장합니다. 처리 전에 해당 ID가 이미 본 적 있는지 확인합니다.
- 삽입 대신 업서트: 중복을 자연스럽게 처리하는 데이터베이스 연산을 사용합니다(예:
INSERT ... ON CONFLICT DO NOTHING). - 멱등성 키: 카드 청구 같은 작업에는 고유 키를 전달해 결제 공급자가 중복 요청을 무시하도록 합니다.
웹훅 소비자를 위한 모범 사례
| 관행 | 중요한 이유 |
|---|---|
| 서명 검증 | 위조 요청 방지 |
| 빠른 응답(2초 이내) | 타임아웃과 불필요한 재시도 방지 |
| 비동기 처리 | 수신과 무거운 처리를 분리 |
| 멱등 핸들러 사용 | 중복 전달에 안전 |
| 모든 이벤트 로깅 | 디버깅과 감사에 도움 |
| 실패 모니터링 | 문제가 확대되기 전에 감지 |
리플레이 공격을 방지하기 위해 서명의 타임스탬프를 검증하는 것도 고려하세요. 많은 공급자가 서명된 페이로드에 타임스탬프를 포함합니다. 몇 분 이상 지난 이벤트는 거부하세요.
웹훅 생산자를 위한 모범 사례
웹훅을 보내는 서비스를 구축한다면 다음 가이드라인을 따르세요.
- 모든 페이로드를 HMAC-SHA256으로 서명합니다.
- 고유 이벤트 ID와 타임스탬프를 포함합니다.
- 지수 백오프와 지터로 재시도합니다.
- 소비자가 전달 로그를 확인하고 이벤트를 수동으로 재생할 수 있는 방법을 제공합니다.
- 서명 검증 과정을 명확히 문서화합니다.
FAQ
서명된 웹훅 페이로드란 무엇인가?
서명된 페이로드는 요청 본문과 공유 비밀 키로 계산된 암호화 서명(보통 HMAC)을 포함합니다. 수신자는 서명을 검증해 요청이 진짜이고 변조되지 않았음을 확인합니다.
웹훅은 몇 번 재시도해야 하나?
보편적인 횟수는 없지만, 대부분의 공급자는 몇 시간에 걸쳐 3~5회 재시도합니다. 그 후 이벤트는 보통 실패로 표시되고 수동 재생이 가능할 수 있습니다. 재시도 횟수와 관계없이 시스템은 중복을 멱등하게 처리해야 합니다.
HTTPS 없이 웹훅을 사용할 수 있나?
기술적으로는 가능하지만 항상 HTTPS를 사용해야 합니다. 웹훅에는 민감한 데이터가 포함되는 경우가 많고, TLS가 없으면 공격자가 페이로드를 가로채거나 변조할 수 있습니다. HTTPS는 의도한 서버로 전송되는 것도 보장합니다.
Nginx 로그로 웹훅 디버깅하기
웹훅이 실패하면 가장 먼저 볼 곳은 웹 서버 로그입니다. Nginx를 사용한다면 액세스 로그와 에러 로그가 상태 코드와 응답 시간을 포함한 모든 요청을 기록합니다. 반복되는 401(서명 검증 실패), 500(애플리케이션 오류), 타임아웃 같은 패턴을 빠르게 발견할 수 있습니다.
더 빠른 분석을 원한다면 Nginx Log Analyzer를 사용해 웹훅 트래픽을 파싱하고 요약하며, 상태 코드로 필터링하고 문제가 있는 엔드포인트를 식별할 수 있습니다.