5분 안에 프로덕션 레디 웹훅 전송 시스템 구축
Source: Dev.to
작동 방식: 흐름
핵심 기능
- ⚡ 즉시 응답 (
202 Accepted) - 🔄 모든 구독자에게 병렬 전달
- 🔐 모든 웹훅에 HMAC SHA‑256 서명
- ♻️ 지수 백오프를 이용한 자동 재시도 (1 초, 2 초, 4 초)
- 📊 상태 모니터링 및 10회 실패 시 자동 비활성화
아웃고잉 웹훅이란?
대부분의 개발자는 Stripe, GitHub, Slack 등에서 수신하는 웹훅에 익숙합니다. 아웃고잉 웹훅(‘리버스 웹훅’이라고도 함)은 이벤트가 발생했을 때 애플리케이션이 외부 서비스에 알릴 수 있게 해줍니다. 사용자가 가입하거나, 주문이 발송되거나, 결제가 완료되는 등 시스템에서 무언가가 일어나면 애플리케이션은 등록된 웹훅 URL로 HTTP POST 요청을 보냅니다.
이는 통합을 구축하고, 실시간 알림을 제공하며, 고객이 플랫폼을 확장할 수 있게 하는 데 필수적입니다.
도전 과제: 제대로 구축하기는 어렵다
처음부터 견고한 웹훅 전달 시스템을 만들려면 여러 복잡한 문제를 해결해야 합니다.
- URL 검증 – 웹훅 URL이 정상인지 확인
- 보안 – 변조 방지를 위한 HMAC 서명
- 신뢰성 – 다운된 엔드포인트를 우아하게 처리
- 확장성 – 수천 명의 구독자에게 차단 없이 전달
- 모니터링 – 전달 통계, 실패 추적, 고장난 웹훅 자동 비활성화
- 재시도 로직 – 지수 백오프, 최대 시도 횟수, 타임아웃 처리
큐 기반 아키텍처: 비밀 소스
확장 가능한 웹훅 전달의 핵심은 이벤트 트리거와 전달을 분리하는 것입니다.
// 1. 감사 로그를 위해 이벤트 저장
await conn.insertOne('events', eventData);
// 2. 일치하는 모든 웹훅에 이벤트 ID 표시
await conn.updateMany(
'webhooks',
{ status: 'active', $or: [{ events: eventType }, { events: '*' }] },
{ $set: { pendingEventId: eventData.id } }
);
// 3. 하나의 원자적 작업으로 모든 웹훅을 큐에 넣기
await conn.enqueueFromQuery(
'webhooks',
{ status: 'active', pendingEventId: eventData.id },
'webhook-delivery'
);
마법은 enqueueFromQuery 에 있습니다. 웹훅을 메모리로 로드하고 반복하는 대신, 다음을 수행합니다.
- 단일 데이터베이스 작업으로 전체를 큐에 삽입
- 메모리 오버헤드 없이 수천 개의 웹훅 처리
- 즉시 반환 (
202 Accepted) - 워커 함수가 병렬로 전달을 처리
이 아키텍처는 구독자 목록이 방대해도 즉시 응답 시간을 보장합니다.
보안 우선: HMAC 서명
각 웹훅 페이로드에는 수신자가 진위 여부를 검증할 수 있도록 암호화 서명이 포함됩니다.
function generateSignature(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const sigBasestring = `${timestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', secret)
.update(sigBasestring, 'utf8')
.digest('hex');
return { signature: `v1=${signature}`, timestamp };
}
각 웹훅에 포함되는 헤더
X-Webhook-Signature: HMAC SHA‑256 서명X-Webhook-Timestamp: Unix 타임스탬프 (재생 공격 방지)X-Webhook-Id: 구독 ID
수신자는 서명을 검증하고, 5분 이상 지난 타임스탬프는 거부하여 재생 공격을 방지합니다.
자동 URL 검증
웹훅 등록을 받기 전에 시스템은 업계 표준 방법으로 URL을 검증합니다.
- Stripe‑스타일 검증 – 검증 토큰이 포함된 테스트 페이로드를 보내고 HTTP 200 응답을 기대
- Slack‑스타일 챌린지 – 무작위 챌린지 문자열을 보내고 응답에 그대로 반환되길 기대
검증은 워커 큐에서 비동기로 실행되므로, 등록 요청은 즉시 반환되고 검증은 백그라운드에서 진행됩니다.
스마트 재시도 로직
전송 실패 시 지수 백오프를 적용한 자동 재시도가 실행됩니다.
{
"maxRetries": 3,
"backoffIntervals": ["1s", "2s", "4s"],
"timeout": "10s",
"autoDisableAfter": 10
}
크론 작업이 30분마다 실행되어 1시간 이상 실패한 웹훅을 재시도함으로써, 고장난 엔드포인트에 대한 과도한 요청을 방지합니다.
이벤트 유연성
일부 시스템이 미리 정의된 이벤트 타입만 허용하는 것과 달리, 이 시스템은 어떤 이벤트 이름이든 지원합니다.
# 전자상거래 이벤트
POST /events/trigger/order.placed
# IoT 이벤트
POST /events/trigger/sensor.temperature.high
# 맞춤 비즈니스 이벤트
POST /events/trigger/report.generated
구독자는 특정 이벤트에만 등록하거나 "*" 와일드카드를 사용해 모든 이벤트를 받을 수 있습니다.
프로덕션 수준 모니터링
시스템은 각 웹훅에 대한 상세 통계를 추적합니다.
{
"deliveryCount": 1247,
"consecutiveFailures": 0,
"lastDeliveryAt": "2025-01-15T10:30:00Z",
"lastDeliveryStatus": "success",
"status": "active"
}
연속 10번 실패하면 웹훅이 자동으로 비활성화되어 리소스를 절약합니다. 사용자는 POST /webhooks/:id/retry 로 수동 재시도도 할 수 있습니다.
전체 API
웹훅 관리를 위한 완전한 CRUD API가 제공됩니다.
# 웹훅 생성 (자동 검증 포함)
POST /webhooks
{
"clientId": "customer-123",
"url": "https://example.com/webhook",
"events": ["order.placed", "order.shipped"],
"verificationType": "stripe"
}
# 웹훅 목록 조회
GET /webhooks?status=active&event=order.placed
# 이벤트 트리거 (모든 구독자에게 전달 큐에 넣음)
POST /events/trigger/order.placed
{
"orderId": "123",
"total": 99.99,
"customer": "john@example.com"
}
# 전달 통계 확인
GET /webhooks/:id/stats
# 실패한 웹훅 수동 재시도
POST /webhooks/:id/retry
멱등성 등록
시스템은 clientId + url 을 복합 키로 사용해 upsert 로직을 구현합니다.
- 첫 번째 등록 시 웹훅이 생성됩니다.
- 동일한
clientId+url로 재등록하면 기존 웹훅이 업데이트됩니다. - 중복 웹훅을 방지합니다.
- 업데이트 시에도 HMAC 비밀키가 유지됩니다.
- 다중 테넌트 SaaS 애플리케이션에 최적입니다.
실제 적용 사례
이 시스템은 인기 워크플로우 도구와 원활히 통합됩니다.
- n8n – 웹훅 트리거를 만들고 URL을 등록하면 워크플로가 자동으로 활성화됩니다.
- Zapier – “Webhooks by Zapier”의 Catch Hook 를 사용하고
verificationType: "stripe"로 URL을 등록하면 Zap이 작동합니다. - 맞춤 앱 – 어떤 언어든 서명 검증을 구현하고 바로 이벤트를 받을 수 있습니다.
실제 동작 확인
직접 사용해 보고 싶나요? 전체 구현은 Codehooks.io 템플릿으로 제공됩니다.
