웹훅 완벽 가이드: 서명된 페이로드와 재시도
결제 게이트웨이를 연동한 후 결제가 성공할 때 데이터베이스를 업데이트해야 한다고 가정해 봅시다. 몇 초마다 API를 폴링하는 것은 비효율적이고 느립니다. 웹훅은 이벤트가 발생할 때 서버로 이벤트를 푸시하여 이 문제를 해결합니다. 하지만 적절한 보안과 안정성 없이는 웹훅이 버그와 취약점의 원인이 될 수 있습니다. 이 글에서는 서명된 페이로드와 재시도에 초점을 맞춰 웹훅을 올바르게 구현하는 방법을 설명합니다.
웹훅이란 무엇인가?
웹훅은 HTTP 콜백입니다. 서비스에서 이벤트가 발생하면 해당 서비스가 사용자가 제공한 URL로 HTTP POST 요청을 보냅니다. 페이로드에는 일반적으로 이벤트에 대한 JSON 데이터가 포함됩니다. 예를 들어 고객이 결제를 완료하면 Stripe는 charge.succeeded 이벤트를 엔드포인트로 전송합니다.
웹훅은 푸시 기반이므로 폴링에 비해 지연 시간과 서버 부하를 줄입니다. 그러나 요청이 진짜인지 어떻게 알 수 있을까요? 이벤트 발생 시 서버가 다운되어 있으면 어떻게 될까요? 이러한 문제를 해결해야 합니다.
서명된 페이로드가 중요한 이유
누구나 웹훅 엔드포인트로 HTTP POST를 보낼 수 있습니다. 검증 없이는 공격자가 이벤트를 위조하여 주문을 결제 완료로 표시하는 등의 무단 작업을 수행할 수 있습니다. 서명된 페이로드는 발신자와 사용자만 생성할 수 있는 암호화 서명을 포함하여 이 문제를 해결합니다.
가장 일반적인 방법은 공유 비밀을 사용하는 HMAC(Hash-based Message Authentication Code)입니다. 발신자는 비밀을 사용하여 페이로드의 해시를 계산하고 헤더에 포함합니다. 사용자는 해시를 다시 계산하여 비교합니다. 일치하면 요청이 진짜입니다.
서명된 페이로드 검증 방법
Node.js와 Express를 사용한 단계별 접근 방식은 다음과 같습니다.
- 원시 요청 본문을 문자열로 가져옵니다. 검증 전에 JSON으로 파싱하지 마세요. 파싱하면 바이트 시퀀스가 변경될 수 있습니다.
- 헤더에서 서명을 추출합니다(예:
X-Signature). - 비밀과 원시 본문을 사용하여 HMAC을 계산합니다.
- 타이밍 공격을 방지하기 위해 상수 시간 비교를 사용하여 계산된 서명과 수신된 서명을 비교합니다.
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.status(200).send('OK');
});
항상 강력한 비밀을 사용하고 주기적으로 교체하세요. 클라이언트 측 코드에 절대 노출하지 마세요.
백오프를 사용한 재시도 구현
웹훅은 인터넷을 통해 전달되므로 실패가 발생합니다. 서버가 다운되었거나 네트워크에 문제가 있을 수 있습니다. 견고한 웹훅 시스템은 지수 백오프를 사용하여 실패한 전달을 재시도합니다.
일반적으로 발신자는 타임아웃(예: 5초) 내에 2xx 응답을 받지 못하면 재시도합니다. 재시도 일정은 즉시, 1분 후, 5분 후, 30분 후, 2시간 후 등 최대 시도 횟수(예: 5회)까지일 수 있습니다.
수신자로서 타임아웃을 피하기 위해 신속하게 응답해야 합니다. 200 OK로 수신을 확인하고 이벤트를 비동기적으로 처리하세요.
멱등성 처리
재시도로 인해 중복 전달이 발생할 수 있으므로 이벤트 처리는 멱등성을 가져야 합니다. 페이로드에 고유 이벤트 ID를 포함하고 처리된 ID를 저장하세요. 처리하기 전에 ID가 이미 처리되었는지 확인하세요.
async function processEvent(event) {
const { id, type, data } = event;
if (await db.processedEvents.findOne({ id })) {
return; // Already processed
}
await db.processedEvents.insertOne({ id });
// Handle event based on type
}
데이터베이스 트랜잭션을 사용하여 성공적으로 처리된 후에만 이벤트가 처리된 것으로 표시되도록 하세요.
웹훅 보안 모범 사례
- 처리 전 항상 서명을 검증하세요.
- HTTPS를 사용하여 도청을 방지하세요.
- 타임스탬프가 포함된 경우 검증하여 재전송 공격을 방지하세요.
- 서비스 거부 공격을 피하기 위해 페이로드 크기를 제한하세요.
- 디버깅과 감사를 위해 모든 웹훅 시도를 로그로 기록하세요.
비교: 폴링 vs 웹훅
| 측면 | 폴링 | 웹훅 |
|---|---|---|
| 지연 시간 | 높음 (간격 기반) | 낮음 (거의 실시간) |
| 서버 부하 | 높음 (지속적인 요청) | 낮음 (이벤트 발생 시에만) |
| 복잡성 | 구현 간단 | 보안 및 재시도 필요 |
| 안정성 | 폴링 빈도에 따라 다름 | 재시도 로직에 따라 다름 |
자주 묻는 질문
로컬에서 웹훅을 테스트하려면 어떻게 해야 하나요?
ngrok과 같은 도구를 사용하여 로컬 서버를 인터넷에 노출하세요. 웹훅 제공자가 ngrok URL로 이벤트를 보내도록 구성하세요.
웹훅 전송 시 서버가 다운되어 있으면 어떻게 되나요?
발신자는 백오프를 사용하여 재시도해야 합니다. 서버가 고가용성을 유지하고 타임아웃을 피하기 위해 신속하게 응답하는지 확인하세요.
HMAC 대신 비대칭 서명을 사용할 수 있나요?
예, 일부 제공자는 RSA 또는 ECDSA 서명을 사용합니다. 공개 키로 검증합니다. HMAC은 더 간단하지만 공유 비밀이 필요합니다.
웹훅 페이로드를 디버깅할 때 JSON을 읽기 쉽게 포맷하면 도움이 됩니다. JSON Formatter를 사용하여 웹훅 페이로드를 즉시 예쁘게 출력하고 검증해 보세요.