웹훅 완벽 가이드: 서명된 페이로드와 재시도
API를 구축했고 이제 이벤트가 발생할 때 다른 서비스에 알려야 합니다. 웹훅 엔드포인트를 설정했지만 곧 누구나 가짜 페이로드를 보낼 수 있고, 서버가 다운되면 이벤트가 영원히 사라진다는 사실을 발견하게 됩니다. 이는 흔한 함정입니다. 웹훅은 개념은 단순하지만 보안과 안정성을 위해 신중한 설계가 필요합니다.
이 가이드에서는 웹훅의 핵심을 살펴봅니다. 페이로드에 서명하여 변조를 방지하는 방법, 수신 측에서 서명을 검증하는 방법, 그리고 지수 백오프를 사용한 재시도 로직으로 장애를 처리하는 방법을 다룹니다. 또한 중복 처리를 피하기 위한 멱등성도 다룹니다. 이 가이드를 마치면 신뢰할 수 있는 견고한 웹훅 시스템을 갖추게 될 것입니다.
웹훅이란 무엇인가?
웹훅은 사용자 정의 HTTP 콜백입니다. 소스 시스템에서 이벤트가 발생하면(예: 결제 처리) 이벤트 세부 정보가 담긴 페이로드와 함께 지정한 URL로 HTTP POST 요청을 보냅니다. 이를 통해 폴링 없이 서비스 간 실시간 통신이 가능합니다.
그러나 웹훅은 두 가지 과제를 제기합니다:
- 보안: 수신자는 요청이 실제로 발신자로부터 온 것임을 어떻게 알 수 있을까요?
- 안정성: 수신자가 일시적으로 사용할 수 없으면 어떻게 될까요? 이벤트가 결국 전달되도록 어떻게 보장할까요?
서명된 페이로드로 웹훅 보호하기
위조 요청을 방지하려면 발신자가 공유 비밀 키를 사용하여 페이로드에 서명해야 합니다. 그러면 수신자는 서명을 검증하여 발신자를 인증할 수 있습니다. 가장 일반적인 방법은 SHA-256을 사용하는 HMAC(Hash-based Message Authentication Code)입니다.
HMAC 서명 작동 방식
- 발신자와 수신자는 비밀 키(이상적으로는 길고 무작위한 문자열)를 공유합니다.
- 각 웹훅에 대해 발신자는 비밀 키를 사용하여 요청 본문의 HMAC을 계산하고 헤더(예:
X-Signature)에 포함합니다. - 수신자는 받은 본문에 대해 동일한 HMAC을 계산하고 헤더 값과 비교합니다. 일치하면 요청은 진본입니다.
JSON 직렬화 차이로 인한 불일치를 피하려면 파싱된 버전이 아닌 원시 요청 본문에 서명하는 것이 중요합니다.
예제: Node.js에서 페이로드 서명하기
const crypto = require('crypto');
function signPayload(payload, secret) {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
// 발신자 측:
const payload = JSON.stringify({ event: 'payment.success', amount: 100 });
const signature = signPayload(payload, 'your-secret-key');
// 헤더에 서명 포함: X-Signature: signature
수신자에서 서명 검증하기
수신 측에서는 페이로드를 처리하기 전에 서명을 검증해야 합니다. 타이밍 공격을 방지하려면 상수 시간 비교를 사용하세요.
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// 웹훅 엔드포인트에서:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.rawBody; // 문자열로 된 원시 본문
if (!verifySignature(payload, signature, 'your-secret-key')) {
return res.status(401).send('Invalid signature');
}
// 이벤트 처리
res.status(200).send('OK');
});
참고: 중간자 공격을 방지하려면 항상 HTTPS를 사용하세요. 또한 재전송 공격을 방지하기 위해 서명된 페이로드에 타임스탬프를 포함하는 것을 고려하세요.
지수 백오프로 재시도 구현하기
안정적인 수신자라도 네트워크 문제나 일시적인 장애로 인해 웹훅 전달이 실패할 수 있습니다. 견고한 발신자는 실패한 전달을 재시도해야 합니다. 그러나 단순한 재시도는 수신자에 과부하를 주거나 중복 처리를 유발할 수 있습니다. 해결책은 지터와 최대 재시도 한도를 갖춘 지수 백오프입니다.
재시도 전략
- 실패 시 재시도: 수신자가 2xx가 아닌 상태 코드를 반환하거나 시간 초과되면 재시도합니다.
- 지수 백오프: 재시도 간 지연을 지수적으로 늘립니다(예: 1초, 2초, 4초, 8초...).
- 지터: 썬더링 허드 문제를 피하기 위해 지연에 무작위 변동을 추가합니다.
- 최대 시도 횟수: 한도(예: 5회 재시도)를 설정한 후 웹훅을 실패로 표시합니다(관리자에게 알림 등).
예제: Node.js에서 재시도 로직
async function deliverWebhook(url, payload, secret, maxRetries = 5) {
const signature = signPayload(payload, secret);
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
body: payload,
});
if (response.ok) return;
throw new Error(`HTTP ${response.status}`);
} catch (error) {
attempt++;
if (attempt >= maxRetries) throw error;
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
이 함수는 지수 백오프와 지터를 사용하여 최대 5회까지 재시도하며 지연을 30초로 제한합니다. 필요에 따라 매개변수를 조정하세요.
멱등성 보장하기
재시도는 중복 전달을 일으킬 수 있으므로 웹훅 핸들러는 멱등해야 합니다. 페이로드에 고유한 이벤트 ID를 포함하고 수신자가 처리된 ID를 추적하여 이중 처리를 피하도록 하세요.
// 수신자에서:
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const { id } = req.body;
if (processedEvents.has(id)) {
return res.status(200).send('Already processed');
}
// 이벤트 처리
processedEvents.add(id);
res.status(200).send('OK');
});
프로덕션에서는 재시작 간에도 처리된 ID를 추적하기 위해 영구 저장소(예: Redis 또는 데이터베이스)를 사용하세요.
비교: 서명된 웹훅 vs. 서명되지 않은 웹훅
| 측면 | 서명되지 않음 | 서명됨 (HMAC) |
|---|---|---|
| 보안 | 누구나 요청을 위조할 수 있음 | 비밀 키 보유자만 유효한 서명을 생성할 수 있음 |
| 무결성 | 페이로드가 변조될 수 있음 | 변조 시 서명이 무효화됨 |
| 복잡성 | 낮음 | 보통 (키 관리, 검증) |
| 사용 사례 | 내부, 신뢰할 수 있는 네트워크 | 모든 공개 웹훅 |
FAQ
웹훅 서명이란 무엇인가요?
웹훅 서명은 공유 비밀 키로 생성된 페이로드의 암호화 해시(보통 HMAC)입니다. 이를 통해 수신자는 요청이 예상 발신자로부터 왔고 페이로드가 변경되지 않았음을 검증할 수 있습니다.
실패한 웹훅을 몇 번 재시도해야 하나요?
정해진 횟수는 없지만 일반적인 관행은 지수 백오프로 3~5회 재시도하는 것입니다. 그 후에는 전달을 실패로 표시하고 팀에 알리세요. 이벤트의 중요성을 고려하세요. 결제 알림은 중요하지 않은 업데이트보다 더 많은 재시도를 필요로 할 수 있습니다.
HTTPS 없이 웹훅을 사용할 수 있나요?
프로덕션에서는 일반 HTTP로 웹훅을 사용해서는 안 됩니다. HTTPS는 전송 중 페이로드를 암호화하여 도청과 중간자 공격을 방지합니다. 서명이 있더라도 HTTP는 민감한 데이터를 노출합니다.
웹훅 페이로드를 테스트할 준비가 되셨나요? 전송하거나 디버깅하기 전에 JSON 페이로드를 예쁘게 출력하고 검증하려면 JSON Formatter를 사용하세요.