웹훅 완벽 가이드: 서명된 페이로드와 재시도
타사 서비스로부터 이벤트를 수신하기 위해 웹훅 엔드포인트를 구축했습니다. 테스트에서는 모든 것이 잘 작동하지만 프로덕션에서는 중복 이벤트, 누락된 알림, 그리고 위조 요청에 대한 보안 우려를 발견하게 됩니다. 이는 적절한 안전 장치 없이 웹훅을 구현할 때 흔히 발생하는 함정입니다.
웹훅은 서비스가 어떤 일이 발생했을 때 애플리케이션에 알릴 수 있도록 하는 HTTP 콜백입니다. 폴링과 달리 실시간 업데이트를 제공합니다. 하지만 안정적으로 사용하려면 두 가지 중요한 측면을 해결해야 합니다: 진위성(요청이 예상된 발신자로부터 왔는지 확인)과 전달(실패 및 재시도 처리)입니다. 이 글에서는 실제 코드 예제와 함께 두 가지를 모두 설명합니다.
웹훅 작동 방식
웹훅은 사용자가 정의한 HTTP POST 엔드포인트입니다. 이벤트가 발생하면 소스 서비스가 해당 URL로 이벤트를 설명하는 페이로드와 함께 요청을 보냅니다. 예를 들어 결제 처리기는 결제가 성공했을 때 웹훅을 보낼 수 있습니다.
흐름은 간단합니다:
- 서비스에 웹훅 URL을 등록합니다.
- 이벤트가 발생하면 서비스가 해당 URL로 HTTP POST를 보냅니다.
- 엔드포인트가 페이로드를 처리하고 수신 확인으로 2xx 상태를 반환합니다.
적절한 검증이 없으면 누구나 엔드포인트로 요청을 보낼 수 있습니다. 재시도 처리가 없으면 일시적인 실패로 이벤트가 손실될 수 있습니다. 각각을 해결해 보겠습니다.
서명된 페이로드로 웹훅 보안 강화
웹훅이 예상된 발신자로부터 왔는지 확인하기 위해 서비스는 공유 비밀 키를 사용하여 페이로드에 서명합니다. 서명은 일반적으로 요청 본문의 HMAC(Hash-based Message Authentication Code)이며, X-Signature와 같은 헤더로 전송됩니다.
HMAC 서명 작동 방식
발신자는 비밀 키와 원시 요청 본문을 사용하여 HMAC을 계산한 다음, 16진수로 인코딩된 서명을 헤더에 포함합니다. 엔드포인트는 동일한 비밀 키로 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;
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);
// ... 이벤트 처리
res.status(200).send('OK');
});
핵심 사항:
- 검증에는 파싱된 객체가 아닌 원시 요청 본문을 항상 사용하세요. 파싱은 페이로드를 변경(예: 공백)하여 서명을 깨뜨릴 수 있습니다.
- 타이밍 공격을 방지하려면 타이밍 안전 비교를 사용하세요.
- 비밀 키를 안전하게 저장(환경 변수, 비밀 관리자)하고 주기적으로 교체하세요.
추가 보안 조치
- 타임스탬프 검증: 서명에 타임스탬프를 포함(예:
t=timestamp,v1=signature)하고 몇 분 이상 지난 요청을 거부하여 재전송 공격을 방지하세요. - IP 허용 목록: 제공자가 IP 범위를 공개하는 경우 엔드포인트를 해당 IP로 제한하세요.
- HTTPS: 전송 중 페이로드를 보호하려면 항상 HTTPS를 사용하세요.
신뢰할 수 있는 재시도 구현
웹훅 전달은 보장되지 않습니다. 네트워크가 실패하거나 서버가 다운되거나 2xx가 아닌 상태를 반환할 수 있습니다. 이를 처리하기 위해 발신자는 일반적으로 지수 백오프로 재시도합니다. 수신자로서 엔드포인트가 멱등성을 갖고 빠르게 응답하도록 설계해야 합니다.
발신자 측 (웹훅을 보내는 경우)
웹훅을 보내는 서비스를 구축하는 경우 백오프를 사용한 재시도를 구현하세요. 일반적인 전략:
- 네트워크 오류 또는 5xx 응답 시 재시도합니다.
- 4xx 응답(429 Too Many Requests 제외)에는 재시도하지 않습니다.
- 지터를 포함한 지수 백오프 사용: 최대값까지 1초, 2초, 4초, 8초 등으로 대기합니다.
- 최대 재시도 횟수(예: 5회)를 설정하고 실패 시 데드 레터 큐를 고려하세요.
Python의 재시도 로직 예제:
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if 200 <= response.status_code < 300:
return True
elif 400 <= response.status_code < 500 and response.status_code != 429:
# 클라이언트 오류, 재시도하지 않음
return False
except RequestException:
pass
# 지터를 포함한 지수 백오프
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
수신자 측
엔드포인트는 다음을 수행해야 합니다:
- 빠르게 확인: 몇 초 이내에 2xx를 반환합니다. 처리가 더 오래 걸리면 이벤트를 큐에 넣고 비동기적으로 처리하세요.
- 멱등성 유지: 페이로드의 이벤트 ID를 사용하여 중복 제거합니다. 처리된 ID를 저장하고 반복을 무시하세요.
- 동시성 처리: 동일한 이벤트를 동시에 처리할 때 경쟁 조건을 피하기 위해 데이터베이스 트랜잭션이나 잠금을 사용하세요.
데이터베이스 고유 제약 조건을 사용한 멱등성 예제:
CREATE TABLE processed_events (
event_id VARCHAR(255) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
처리하기 전에 이벤트 ID를 삽입해 보세요. 기본 키를 위반하면 처리를 건너뜁니다.
로컬에서 웹훅 테스트
개발 중에는 서비스가 로컬 서버에 도달할 수 있도록 공개 URL이 필요합니다. ngrok 또는 localtunnel과 같은 도구가 localhost를 노출합니다. 개발 중에도 항상 테스트 비밀 키를 사용하고 서명을 검증하여 문제를 조기에 발견하세요.
비교: 폴링 vs 웹훅
| 측면 | 폴링 | 웹훅 |
|---|---|---|
| 지연 시간 | 간격에 따라 다름 | 거의 실시간 |
| 서버 부하 | 높음 (빈번한 요청) | 낮음 (이벤트 발생 시에만) |
| 복잡성 | 구현 간단 | 서명 검증 및 재시도 처리 필요 |
| 신뢰성 | 재시도를 직접 제어 | 발신자의 재시도 정책에 의존 |
FAQ
서명된 웹훅 페이로드란 무엇인가요?
서명된 페이로드는 공유 비밀 키로 생성된 암호화 서명(보통 HMAC)을 포함합니다. 이를 통해 수신자는 요청이 예상된 발신자로부터 왔고 변조되지 않았음을 확인할 수 있습니다.
중복 웹훅 전달을 어떻게 처리하나요?
페이로드의 고유 이벤트 ID를 사용하여 엔드포인트를 멱등성 있게 만드세요. 처리된 ID를 저장하고 중복을 무시하세요. 이렇게 하면 재시도나 네트워크 문제로 인한 이중 처리를 방지할 수 있습니다.
실패한 웹훅 전달을 재시도해야 하나요?
예, 하지만 일시적인 오류(네트워크 문제, 5xx)에만 해당합니다. 지터를 포함한 지수 백오프를 사용하고 최대 재시도 횟수를 설정하세요. 4xx 오류(429 제외)는 요청 자체의 문제를 나타내므로 재시도하지 마세요.
웹훅 페이로드를 디버깅할 때 JSON 데이터를 검사해야 하는 경우가 많습니다. JSON Formatter를 사용하면 JSON 페이로드를 빠르게 예쁘게 출력하고 유효성을 검사할 수 있습니다.