블로그TECH NOTE
개발

웹훅 중복 처리 방지: 자동화가 두 번 실행되지 않게 설계하는 법

이 글에서 해결할 것

웹훅은 같은 이벤트가 다시 올 수 있다는 전제로 설계해야 합니다. 서명 검증, 고유 이벤트 저장, 비동기 처리, 재처리 절차를 한 흐름으로 정리합니다.

POEMORA · 편집팀2026-09-128
동일한 두 이벤트 캡슐 중 하나만 보안 게이트를 지나 처리 대기열로 들어가는 장면
중복 이벤트를 입구에서 식별하고 한 번의 업무 실행으로 연결하는 구조를 표현했습니다.
#웹훅 중복 처리#웹훅 멱등성#자동화 오류 방지#이벤트 중복 제거#웹훅 보안

읽기 전에 핵심만

  1. 웹훅은 중복 수신을 정상 조건으로 보고, 공급자와 이벤트 ID의 고유 제약으로 첫 관문을 막아야 합니다.
  2. 수신 저장과 실제 업무 실행을 분리하면 빠르게 응답하면서도 실패한 작업을 안전하게 다시 처리할 수 있습니다.
  3. 이벤트 중복 제거와 별도로 주문 상태·알림 발송 같은 업무 결과에도 고유성과 허용 전이 규칙이 필요합니다.

결제 완료 알림이나 고객 문의 연동이 두 번 실행되지 않게 하려면 웹훅을 한 번만 온다고 믿어서는 안 됩니다. 요청 서명을 확인한 뒤 공급자와 이벤트 ID의 조합을 고유하게 저장하고, 실제 업무는 큐에서 비동기로 처리하세요. 같은 이벤트가 다시 오면 성공 응답만 돌려주고 작업은 반복하지 않는 구조가 기본입니다.

웹훅 중복은 예외가 아니라 설계 조건입니다

웹훅 발신자는 수신 서버의 응답을 받지 못하면 전송을 다시 시도할 수 있습니다. 실제 처리는 끝났지만 네트워크에서 응답만 사라져도 발신자는 실패로 판단합니다. Stripe는 일시적으로 처리하지 못한 이벤트를 최대 3일 동안 자동 재전송한다고 안내합니다.[5] Microsoft의 Event Hubs 운영 지침도 최소 한 번 전달을 전제로 소비자가 중복 입력을 멱등하게 처리해야 한다고 설명합니다.[3] 같은 원칙을 Microsoft Fabric 문서는 이벤트 ID와 출처 조합의 중복 제거 절차로 구체화합니다.[6]

이 성질을 무시하면 결제 확인 한 건이 주문 두 건, 알림 두 통, 재고 두 번 차감으로 번질 수 있습니다. 반대로 중복을 정상 입력으로 취급하면 재시도는 장애 복구 수단이 됩니다.

설계 기준: 웹훅 호출 횟수가 아니라 업무 결과의 고유성을 보장합니다.

구분위험한 처리권장 처리
이벤트 식별요청 시각만 기록공급자와 이벤트 ID를 함께 저장
응답모든 업무가 끝난 뒤 응답검증과 수신 저장 후 빠르게 성공 응답
재시도무조건 다시 실행처리 상태를 확인하고 미완료 작업만 재개
외부 작업즉시 메일·문자 발송고유 작업을 큐나 outbox에 등록

수신 단계와 업무 실행 단계를 분리하세요

검증 게이트와 수신 저장소, 작업 대기열, 완료 결과가 분리된 자동화 처리 모형
웹훅 수신과 실제 업무 실행을 분리하면 재시도와 장애 복구가 단순해집니다.

GitHub는 웹훅을 받은 서버가 10초 안에 2XX 응답을 보내도록 권장하며, 오래 걸리는 처리는 큐로 넘기는 방식을 제시합니다.[2] Stripe도 동기 처리 대신 비동기 큐 사용을 권장합니다.[1] 빠른 응답 자체보다 중요한 점은 수신과 실행의 실패 경계를 나누는 것입니다.

  1. 원본 요청 본문과 서명을 검증합니다.
  2. 이벤트 종류와 업무에 필요한 필드를 확인합니다.
  3. provider + event_id를 고유 키로 수신함 테이블에 저장합니다.
  4. 새 이벤트일 때만 처리 작업을 큐에 넣습니다.
  5. 저장이 끝나면 2XX를 응답합니다.
  6. 작업자는 업무 고유 키를 확인한 뒤 실제 변경을 수행합니다.

서명 검증은 파싱하거나 내용을 바꾸기 전의 원본 본문으로 해야 합니다. Stripe는 원본 본문이 변형되면 검증이 실패한다고 설명합니다.[1] GitHub는 X-Hub-Signature-256의 HMAC-SHA256 서명을 계산해 안전한 비교 함수로 대조하도록 안내합니다.[4]

고유 키 하나로는 부족할 때가 있습니다

이벤트 ID 중복 제거는 같은 전달을 걸러냅니다. 하지만 서로 다른 이벤트가 같은 업무 결과를 요구할 수도 있습니다. 예를 들어 결제 공급자가 서로 다른 알림을 보냈더라도 주문 한 건의 결제 완료 상태 전환은 한 번만 일어나야 합니다.

서로 다른 종류의 고유성을 따로 둡니다.

  • 수신 고유 키: provider + event_id
  • 업무 고유 키: order_id + payment_completed처럼 결과를 대표하는 값
  • 발송 고유 키: event_id + action_type처럼 메일·문자 작업을 대표하는 값

다음은 개념을 보여 주는 예시입니다. 데이터베이스의 고유 제약이 동시에 들어온 두 요청 중 하나만 새 이벤트로 받아들이게 합니다.

sql
CREATE TABLE webhook_inbox (
  provider TEXT NOT NULL,
  event_id TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (provider, event_id)
);

애플리케이션에서 먼저 조회하고 없으면 저장만 하면 두 요청이 같은 순간에 조회를 통과할 수 있습니다. 최종 방어선은 데이터베이스의 고유 제약이어야 합니다. 충돌한 요청에는 이미 받은 이벤트로 간주해 2XX를 응답하면 됩니다.

처리 상태는 pending과 processed만 두지 마세요

실행 중 프로세스가 멈추면 pending인지 processed인지로는 현재 상황을 설명하기 어렵습니다. Stripe의 미전달 이벤트 처리 예시도 이벤트가 처리 중인지 또는 처리 완료인지 데이터베이스에서 확인하고, 이미 끝난 이벤트는 무시한 채 성공 응답을 반환하는 흐름을 사용합니다.[5]

운영에서는 아래 상태가 실용적입니다.

  • pending: 검증을 마치고 실행 대기 중
  • processing: 작업자가 점유했으며 점유 만료 시각이 있음
  • processed: 업무 결과와 완료 시각이 기록됨
  • failed: 재시도 횟수와 마지막 비밀값 없는 오류가 기록됨
  • dead_letter: 자동 재시도를 멈추고 사람이 확인해야 함

작업자가 멈췄을 때는 점유 만료 후 다시 가져갈 수 있어야 합니다. 다만 메일이나 문자처럼 외부로 나간 작업은 서버 트랜잭션만으로 되돌릴 수 없습니다. 이 경우 outbox에 발송 작업을 고유하게 기록하고, 발송 공급자가 멱등 키를 지원한다면 함께 사용합니다.

순서가 바뀌어도 현재 상태를 지켜야 합니다

이벤트가 생성 순서대로 도착한다고 가정하면 늦게 도착한 과거 이벤트가 최신 상태를 덮을 수 있습니다. Microsoft Fabric은 이벤트 순서를 보장하지 않으므로 수신 순서에 의존하지 말라고 안내합니다.[6]

상태 변경 이벤트는 다음 중 한 가지 기준을 정해 처리합니다.

  • 공급자가 제공하는 객체 버전이나 변경 번호를 비교합니다.
  • 이벤트를 받은 뒤 공급자 API에서 현재 객체 상태를 다시 조회합니다.
  • 되돌릴 수 없는 전이는 상태 머신으로 막습니다. 예를 들어 paid 주문을 오래된 pending 이벤트가 되돌리지 못하게 합니다.

이 판단 기준은 이벤트 종류마다 달라집니다. 모든 웹훅에 같은 로직을 억지로 적용하기보다, 주문·문의·회원 같은 업무 단위별 허용 전이를 문서로 남기는 편이 안전합니다. 자동화 흐름을 설계하면서 수신, 판단, 실행, 복구를 함께 정리해야 한다면 POEMORA 업무 자동화에서 구현 범위를 확인할 수 있습니다.

운영 전에는 재전송을 직접 시험하세요

정상 요청 한 번만 보내는 테스트로는 중복 처리 결함을 찾기 어렵습니다. 배포 전 점검은 실제 실패 순서를 흉내 내야 합니다.

  • [ ] 같은 이벤트를 동시에 두 번 보내도 업무 결과가 한 건인가
  • [ ] 서명이 없거나 틀린 요청은 저장 전에 거절되는가
  • [ ] 수신 저장 뒤 작업자가 멈춰도 다시 이어지는가
  • [ ] 처리 완료 뒤 같은 이벤트를 보내면 2XX만 반환하는가
  • [ ] 오래된 이벤트가 최신 상태를 되돌리지 않는가
  • [ ] 실패 횟수, 마지막 오류, 재처리 결과를 운영자가 볼 수 있는가
  • [ ] 비밀값과 전체 개인정보가 로그에 남지 않는가

GitHub는 재전송 요청에도 원래 전달과 같은 X-GitHub-Delivery 값을 사용한다고 설명합니다.[2] 이런 고유 식별자를 보존한 채 재전송하면 중복 제거 로직을 실제 조건에 가깝게 검증할 수 있습니다.

웹훅 자동화는 호출 누락뿐 아니라 반복 수신, 순서 변경, 중간 중단까지 견뎌야 합니다. 수신함의 고유 제약, 비동기 작업, 업무 상태 전이, 재처리 화면을 함께 설계하면 재시도를 이용해 멈춘 업무를 복구할 수 있습니다.

FAQ

자주 묻는 질문

웹훅이 같은 이벤트를 두 번 보내는 것은 오류인가요?

항상 공급자 오류라고 볼 수는 없습니다. 수신 서버의 응답이 사라지거나 처리 시간이 길면 발신자가 재시도할 수 있습니다. 따라서 수신 측은 같은 이벤트가 여러 번 와도 결과가 달라지지 않게 설계해야 합니다.

이벤트 ID는 메모리나 Redis에만 저장해도 되나요?

짧은 중복 차단에는 캐시가 도움이 되지만, 재시도 기간보다 먼저 만료되거나 장애 때 기록이 사라질 수 있습니다. 결제나 주문처럼 중요한 업무는 데이터베이스의 고유 제약과 처리 상태를 기준으로 두는 편이 안전합니다.

웹훅 처리 후 언제 2XX를 응답해야 하나요?

서명과 입력을 검증하고 이벤트를 내구성 있는 저장소에 기록한 직후 응답하는 방식이 일반적입니다. 메일 발송이나 외부 API 호출처럼 오래 걸리는 작업은 큐에서 처리하고, 실패 상태와 재시도 횟수를 따로 남깁니다.

이미 처리한 이벤트가 다시 오면 어떤 응답을 보내야 하나요?

고유 키로 이미 처리했음을 확인했다면 업무를 다시 실행하지 않고 성공 응답을 보내는 편이 좋습니다. 그래야 발신자가 같은 이벤트를 실패로 판단해 계속 재전송하는 상황을 줄일 수 있습니다.

REFERENCES

확인한 자료

관련 글