웹훅(Webhooks) 완벽 이해

서버 간 HTTP 콜백으로 이벤트를 전달하는 웹훅의 서명 검증(HMAC), 재시도·멱등성, 순서 보장, 2xx 확인 응답, 리플레이 방지, 디버깅까지 정리합니다.

웹훅(Webhook)은 어떤 이벤트가 발생했을 때 서버가 다른 서버로 HTTP 요청을 먼저 보내 알려주는 서버 대 서버 콜백입니다. 클라이언트가 주기적으로 물어보는 폴링(polling)과 반대로, 이벤트가 생긴 쪽(제공자, provider)이 미리 등록된 수신 URL(consumer endpoint)로 POST를 밀어 넣습니다. 결제 완료, 배포 성공, 리포지토리 푸시 같은 사건이 생기면 실시간에 가깝게 통지가 오고, 수신자는 서버를 붙잡고 기다릴 필요가 없어집니다.

이벤트 전달의 형태

제공자는 대개 이벤트 종류(type), 고유 이벤트 ID, 발생 시각, 그리고 페이로드(data)를 담은 JSON 본문을 POST합니다. 수신자는 이 요청을 받고 빠르게 2xx를 돌려주어 접수(ack)를 확인해야 합니다. 표준적인 웹훅 전달 요청은 다음과 같은 모습입니다.

POST /webhooks/payments HTTP/1.1
Host: consumer.example.com
Content-Type: application/json
Webhook-Id: evt_1Qk2m9
Webhook-Timestamp: 1737100800
Webhook-Signature: v1,3Qb1f8y2Y0p9d7c6...

{"type":"payment.succeeded","id":"evt_1Qk2m9","created":1737100800,"data":{"amount":5000,"currency":"krw"}}

서명과 검증 (HMAC)

수신 엔드포인트는 인터넷에 공개돼 있으므로 누구나 가짜 요청을 보낼 수 있습니다. 그래서 제공자는 공유 비밀 키(signing secret)로 요청 본문에 대한 HMAC 서명을 계산해 Webhook-Signature 같은 헤더에 실어 보내고, 수신자는 같은 키로 서명을 다시 계산해 일치하는지 확인합니다. 서명은 보통 타임스탬프와 본문을 이어 붙인 문자열에 대해 계산합니다.

signed_payload = webhook_id + "." + timestamp + "." + raw_body
expected = base64(HMAC_SHA256(signing_secret, signed_payload))
# 헤더의 서명과 expected 를 상수 시간(constant-time) 비교로 대조

검증에서 흔히 하는 실수 두 가지가 있습니다. 첫째, 프레임워크가 파싱해 재직렬화한 JSON이 아니라 수신한 원본 바이트(raw body) 를 그대로 HMAC에 넣어야 합니다. 재직렬화하면 키 순서·공백이 달라져 서명이 어긋납니다. 둘째, 문자열 비교는 타이밍 공격을 피하려고 상수 시간 비교 함수를 써야 합니다.

리플레이 방지 — 공격자가 유효한 요청을 그대로 가로챘다가 나중에 다시 보내는 리플레이(replay) 공격을 막으려면, 서명 대상에 타임스탬프를 포함시키고 수신자가 그 값을 검사해야 합니다. 서명이 유효하더라도 타임스탬프가 현재 시각과 허용 오차(예: 5분)를 벗어나면 거부합니다. 이렇게 하면 과거에 캡처한 요청을 재전송해도 시간 창을 벗어나 통과하지 못합니다.

재시도와 멱등성

네트워크는 불안정하므로 제공자는 수신자가 2xx로 응답하지 않으면(타임아웃·5xx·연결 실패) 지수 백오프로 재시도합니다. 문제는 '수신자는 정상 처리했는데 응답이 유실된' 경우 같은 이벤트가 두 번 도착할 수 있다는 점입니다. 따라서 수신 처리는 반드시 멱등(idempotent) 이어야 합니다.

순서 보장 — 웹훅은 순서를 보장하지 않는 것이 기본입니다. 재시도와 병렬 전송 때문에 created 순서와 도착 순서가 어긋날 수 있습니다. 따라서 '삭제 이벤트가 생성 이벤트보다 먼저 도착'하는 상황도 견뎌야 합니다. 실무에서는 이벤트의 타임스탬프나 리소스의 버전 번호를 보고, 더 오래된 상태로 덮어쓰지 않도록 방어합니다. 순서가 정말 중요하다면 이벤트마다 최신 상태를 API로 다시 조회(reconcile)하는 편이 안전합니다.

디버깅 — 웹훅은 수신자가 인터넷에서 접근 가능해야 하므로 로컬 개발이 까다롭습니다. 개발 중에는 터널링 도구로 로컬 서버를 임시 공개 URL로 노출하거나, 제공자 대시보드의 전송 로그·재전송(replay) 버튼을 활용하세요. 문제를 좁힐 때는 (1) 요청이 도달했는지, (2) 서명 검증이 통과하는지, (3) 2xx를 제때 반환했는지 순서로 확인합니다. 서명 불일치는 대부분 raw body 문제이고, 재시도 폭주는 대개 느린 ack가 원인입니다.