웹훅 완벽 가이드: 서명된 페이로드와 재시도
결제 게이트웨이나 CI/CD 서비스를 통합했고, 이제 실시간 이벤트를 받아야 합니다. 웹훅은 표준 솔루션이지만 두 가지 큰 과제가 있습니다. 요청이 실제로 예상된 발신자로부터 왔는지 확인하는 것과 엔드포인트가 다운되거나 느릴 때 실패를 처리하는 것입니다. 이 글에서는 서명된 웹훅 페이로드와 강력한 재시도 로직을 구현하는 방법을 실제 코드 예제와 함께 설명합니다.
웹훅이란?
웹훅은 사용자 정의 HTTP 콜백입니다. 애플리케이션이 업데이트를 위해 API를 폴링하는 대신, 공급자가 이벤트가 발생할 때마다 지정한 URL로 HTTP POST 요청을 보냅니다. 이를 통해 통합이 효율적이고 거의 실시간으로 이루어집니다.
그러나 웹훅은 푸시 기반이므로 엔드포인트가 공개적으로 접근 가능해야 하고, 예상치 못한 부하를 처리하며, 중복되거나 악의적인 요청을 처리해야 합니다. 바로 여기서 서명과 재시도가 필요합니다.
웹훅 페이로드에 서명하는 이유
검증 없이는 웹훅 URL을 아는 누구나 가짜 이벤트를 보낼 수 있습니다. 공유 비밀로 페이로드에 서명하면 발신자를 인증하고 페이로드가 변조되지 않았음을 확인할 수 있습니다.
가장 일반적인 접근 방식은 SHA-256을 사용한 HMAC(해시 기반 메시지 인증 코드)입니다. 공급자는 비밀 키를 사용하여 원시 요청 본문에 대한 서명을 계산하고 이를 헤더(예: X-Signature)에 포함합니다. 서버는 서명을 다시 계산하여 비교합니다.
HMAC 검증 구현
Express를 사용한 Node.js의 간단한 예제입니다:
const crypto = require('crypto');
const express = require('express');
const app = express();
// 서명 검증을 위해 원시 본문 사용
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (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);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Webhook server running'));
핵심 포인트:
- 원시 본문 사용: 본문을 JSON이 아닌 원시 바이트로 파싱하세요. 변환이 있으면 서명이 변경됩니다.
- 타이밍 안전 비교: 타이밍 공격을 방지하려면
crypto.timingSafeEqual을 사용하세요. - 비밀 관리: 비밀을 안전하게 저장하세요(환경 변수, 비밀 관리자).
재시도 처리: 웹훅을 안정적으로 만들기
네트워크는 실패하고, 서버는 충돌하며, 배포로 인해 다운타임이 발생합니다. 안정적인 웹훅 시스템은 실패한 전달을 재시도해야 합니다. 그러나 순진한 재시도는 중복 처리를 초래하고 시스템에 과부하를 줄 수 있습니다.
재시도 전략
일반적인 재시도 패턴은 다음과 같습니다:
- 지수 백오프: 각 재시도 사이에 더 오래 기다립니다(예: 1초, 2초, 4초, 8초...).
- 최대 재시도 횟수: N번 시도 후 중지하고(예: 5회) 전달을 실패로 표시합니다.
- 지터: 썬더링 허드 문제를 피하기 위해 무작위성을 추가합니다.
대부분의 공급자는 24시간 동안 지수 백오프로 최대 5회 재시도하는 방식을 구현합니다.
멱등성: 안전한 재시도의 핵심
재시도로 인해 동일한 이벤트가 여러 번 전달될 수 있으므로 엔드포인트는 멱등성을 가져야 합니다. 페이로드에 고유한 이벤트 ID를 포함하고 처리된 ID를 저장하여 중복을 건너뛰세요.
간단한 인메모리 세트를 사용한 예제(프로덕션에서는 데이터베이스 사용):
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
// ... 서명 검증 ...
const event = JSON.parse(payload);
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// 이벤트 처리
processedEvents.add(event.id);
res.status(200).send('OK');
});
프로덕션에서는 이벤트 ID에 고유 제약 조건이 있는 데이터베이스를 사용하세요.
웹훅 소비자를 위한 모범 사례
- 빠르게 응답: 무거운 처리를 하기 전에 2xx 상태로 수신 확인을 보내세요. 이벤트를 비동기 처리를 위해 큐에 넣으세요.
- 타임스탬프 검증: 공급자가 타임스탬프를 포함하는 경우 재생 공격을 방지하기 위해 몇 분 이상 지난 요청을 거부하세요.
- HTTPS 사용: 전송 중 페이로드를 보호하기 위해 항상 웹훅 엔드포인트를 HTTPS로 노출하세요.
- 모든 것을 로깅: 디버깅을 위해 수신 요청, 서명 및 처리 결과를 로깅하세요.
- 실패 모니터링: 반복적인 전달 실패에 대한 경고를 설정하세요.
재시도 정책 비교
| 정책 | 장점 | 단점 |
|---|---|---|
| 고정 간격 | 구현이 간단함 | 복구 중인 시스템에 과부하를 줄 수 있음 |
| 지수 백오프 | 장애 중 부하 감소 | 일시적 실패 시 전달 지연 |
| 지터를 포함한 지수 백오프 | 동기화된 재시도 방지 | 약간 더 복잡함 |
FAQ
웹훅 서명을 어떻게 검증하나요?
공유 비밀을 사용하여 원시 요청 본문의 HMAC을 계산하고 타이밍 안전 함수를 사용하여 서명 헤더와 비교하세요. 단순 동등 비교는 절대 사용하지 마세요.
웹훅 엔드포인트가 다운되면 어떻게 해야 하나요?
공급자는 지수 백오프로 재시도해야 합니다. 엔드포인트가 빠르게 2xx를 반환하고 이벤트를 비동기적으로 처리하여 다운타임 영향을 최소화하도록 하세요.
서명 없이 웹훅을 사용할 수 있나요?
권장하지 않습니다. 서명이 없으면 발신자를 검증할 수 없어 엔드포인트가 가짜 이벤트에 취약해집니다. 가능하면 항상 서명을 사용하세요.
웹훅 페이로드를 테스트할 때 JSON 데이터를 검사해야 할 수 있습니다. JSON Formatter를 사용하여 페이로드를 빠르게 예쁘게 출력하고 유효성을 검사하세요.