[백엔드] 웹훅 연동의 함정: 중복 수신, 순서 역전, 유실 그리고 대사(reconciliation)
![[백엔드] 웹훅 연동의 함정: 중복 수신, 순서 역전, 유실 그리고 대사(reconciliation)](https://blog.kakaocdn.net/dna/ltcpK/dJMcaaS6kmc/AAAAAAAAAAAAAAAAAAAAAGWd0OsFFpJ_O9h2VsWy2Dc1-HGL_yt29HoSItLfZJs7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1790780399&allow_ip=&allow_referer=&signature=wDXrVwS5w33UCY1P%2Fu38oMuI%2FuE%3D)

[백엔드] 웹훅 연동의 함정: 중복 수신, 순서 역전, 유실 그리고 대사(reconciliation)
결제사·외부 플랫폼 연동의 절반은 웹훅 받기예요. "이벤트가 오면 처리한다" — 단순해 보이는데, 막상 운영하면 같은 웹훅이 두 번 와요. 취소가 승인보다 먼저 와요. 와야 할 게 안 와요. 웹훅은 친절한 알림이지 신뢰할 수 있는 전달 채널이 아니거든요.
이 글은 웹훅의 3대 불확실성(중복·순서 역전·유실), 수신 처리의 정석(빠른 ACK + 멱등 처리), 그리고 돈이 걸린 연동의 필수 장치인 대사(reconciliation)까지 짚어볼게요. 함정 시리즈 7편이에요.
01. 웹훅을 믿으면 안 되는 세 가지 이유

- 중복 — 상대도 그 비대칭을 똑같이 겪어요. 내가 200을 줬는데 그 응답이 유실되면, 상대는 "전달 실패"로 보고 재전송해요. 성실한 외부사일수록 재시도를 잘 하니까, 중복은 연동에서 늘 깔리는 기본 조건이에요.
- 순서 역전 — "승인 → 취소" 순서로 발생한 이벤트가 "취소 → 승인" 순서로 도착할 수 있어요. 앞 이벤트가 실패해 재시도되는 사이 뒤 이벤트가 먼저 성공하는, 카프카에서 본 그 역학이에요. 발생 순서와 도착 순서는 다른 거예요.
- 유실 — 상대 장애, 내 배포 순간의 5xx, 재전송 한도 소진... 영영 안 오는 웹훅도 있어요. "웹훅만 믿고 상태를 확정"하면 그 구멍이 그대로 데이터 불일치가 돼요.
요컨대 웹훅 수신자는 외부 회사를 브로커로 둔 at-least-once 컨슈머예요. 카프카 시리즈에서 배운 규율이 그대로 적용돼요 — 단지 상대 설정을 우리가 못 만질 뿐이죠.
02. 수신의 정석, 빨리 받고 비동기로 처리

① 서명 검증부터
웹훅 엔드포인트는 공개 URL이에요. 아무나 가짜 "결제 완료"를 쏠 수 있죠. 상대가 제공하는 시크릿 기반 서명(HMAC 등)을 반드시 검증해요. 이거 없는 웹훅 수신은 보안 사고 대기예요.
② 저장하고 즉시 200
흔한 실수가 웹훅 핸들러 안에서 무거운 처리(DB 갱신 + 알림 + 후속 API)를 동기로 다 하는 거예요. 처리가 길어지면 상대 입장에선 타임아웃 → 재전송 → 중복 증폭의 악순환이에요. 정석은 받자마자 원본을 저장(DB나 큐)하고 즉시 200 — "잘 받았다"와 "잘 처리했다"를 분리하는 거예요. 처리 실패는 내 쪽 재시도로 풀고요. 어디서 봤죠? Streams·카프카 컨슈머의 ack 분리와 같은 사고방식이에요.
③ 이벤트 ID로 멱등 처리
상대가 주는 이벤트 고유 ID(없으면 본문 해시)로 처리 이력을 남기고 이미 처리한 ID는 건너뛰어요. 컨슈머 멱등성의 그 방법들 그대로요. 단, "이미 처리한 이벤트"에도 200을 응답해야 해요 — 에러를 주면 상대가 또 재전송하니까요.
④ 순서 역전은 상태 머신으로
"이벤트를 도착 순서대로 적용"하면 역전에 무너져요. 대신 상태 전이 규칙으로 판정해요 — 현재 상태와 이벤트의 조합이 유효한지 봐요. 유효하지 않으면(승인 없는 취소) 보류했다가 재평가하거나, 이벤트의 타임스탬프·버전으로 "더 옛날 이벤트의 늦은 도착"을 무시해요. "취소된 주문에 승인이 늦게 와도 상태가 안 되돌아가는" 게 규칙으로 보장되는 거예요.
03. 대사, 웹훅은 알림이고 진실은 조회로

유실 문제의 근본 해법은 웹훅을 더 잘 받는 게 아니라, 웹훅 없이도 진실에 도달하는 경로를 따로 두는 거예요. 두 경로 구조예요.
- 실시간 경로 — 웹훅. 빠르게 상태를 반영하지만 유실될 수 있어요.
- 보정 경로 — 주기 배치가 상대의 조회 API로 직접 물어봐요. 특히 "PENDING 상태로 N분 넘게 머문 건"을 조회해서 확정해요. 웹훅이 안 왔어도 결국 맞는 상태로 수렴해요.
이게 대사(reconciliation)예요. 결제·정산처럼 돈이 걸린 연동에서는 옵션이 아니라 필수고요. 그리고 대사에서 발견되는 불일치 건수를 지표로 걸어두면 — 웹훅 유실이나 내 처리 버그의 조기 경보가 돼요. 평소 0이던 불일치가 늘기 시작하면 뭔가 터지고 있는 거예요.
04. 보내는 쪽이 될 때도 같은 예의를
거꾸로 우리가 웹훅을 발송하는 입장이면, 받는 쪽이 위 가정을 할 수 있게 해주는 게 예의예요 — 이벤트 고유 ID와 타임스탬프를 싣고, 서명을 제공하고, 실패 시 백오프 재시도를 하되 한도를 두고, 놓친 이벤트를 조회할 API를 같이 열어주는 것. 그리고 발송 자체는 아웃박스로 "커밋됐으면 반드시 발송"을 보장하고요. 좋은 웹훅 공급자의 체크리스트가 곧 좋은 수신자의 가정 목록이에요.
05. 자주 만나는 문제
같은 결제 완료 처리가 두 번 됐어요
이벤트 ID 멱등 처리가 없는 거예요. 처리 이력 테이블(유니크 제약)을 깔고 중복 수신에도 200을 응답해요.
취소했는데 상태가 다시 승인으로 돌아갔어요
늦게 도착한 옛 이벤트가 상태를 덮은 거예요. 상태 전이 규칙(최종 상태에서 역행 금지)과 이벤트 타임스탬프 비교를 넣어요.
웹훅이 안 와서 주문이 PENDING에 멈춰 있어요
유실이에요. "N분 넘은 PENDING 건 조회·확정" 대사 배치가 답이에요. 이미 있다면 그 주기·범위를 점검해요.
웹훅 핸들러가 느려서 상대가 계속 재전송해요
동기 처리 과적이에요. 저장 후 즉시 200 + 비동기 처리로 분리해요.
정리
웹훅 연동의 본질은 한 문장이에요 — 웹훅은 빠른 알림일 뿐, 진실은 멱등한 처리와 대사가 만든다. 그래서 서명을 검증해요. 저장 후 즉시 200을 주고(처리는 비동기로) 이벤트 ID로 멱등하게 처리해요. 순서는 도착 순서가 아니라 상태 전이 규칙으로 판정해요. 그리고 유실은 대사(reconciliation)로 보정하고 그 불일치 건수를 조기 경보 지표로 걸어둬요. 결국 카프카 컨슈머의 규율을 회사 바깥 경계에 그대로 적용하는 일이에요.
여기까지가 "받는 쪽"의 함정이었어요. 다음은 모든 게 터진 다음에 봐야 하는 곳 — JVM 메모리·스레드 진단으로, 함정 시리즈를 마무리합니다.
출처: Stripe — Webhooks best practices · webhooks.fyi — Webhook Security & Reliability
'백엔드' 카테고리의 다른 글
| [백엔드] 무중단 배포의 함정: 구버전·신버전 공존, DB 스키마 변경(expand-contract), graceful shutdown (0) | 2026.08.18 |
|---|---|
| [백엔드] 기술면접 단골 주제 지도: JPA, Kafka, Redis, MySQL, Spring 질문 뼈대와 깊이 글 정리 (0) | 2026.08.04 |
| [Redis] 실무 가이드 지도: 캐싱, 분산 락, 레이트 리미팅, 운영 트러블슈팅 12편 정리 (0) | 2026.07.06 |
| [Kafka] 스프링 카프카 실무 가이드 지도: 설정, 유실 방지, 리밸런싱, 트러블슈팅 11편 정리 (0) | 2026.07.03 |
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."