Webhook 이벤트 전달, 타임아웃, 중복 이벤트 식별, 재시도 결과를 분리한 처리 흐름도

API는 200 OK인데 왜 두 번 처리될까? 웹훅 중복 이벤트와 재시도 설계

Posted by

API 연동을 처음 구현하면 HTTP 요청에 200 OK가 돌아오는 것을 성공으로 생각하기 쉽습니다. 하지만 운영 환경에서는 응답이 성공했는데 동일한 이벤트가 다시 도착하거나, 요청은 실패한 것처럼 보였는데 서버 쪽 작업은 이미 실행된 상황이 생깁니다. 이때 웹훅 중복 처리를 막지 않으면 같은 주문이 두 번 등록되거나 알림이 여러 번 발송될 수 있습니다.

기존의 API란 무엇인가?에서는 엔드포인트, HTTP 메서드, 헤더, 본문 같은 기본 구조를 설명했습니다. 이번에는 그 다음 단계인 Webhook Reliability(웹훅 신뢰성)를 살펴보고자 합니다.


200 OK가 보장하는 것은 어디까지인가?

웹훅 전송자가 200 응답을 받았다는 것은 일반적으로 수신 서버가 해당 요청을 성공으로 응답했다는 사실을 의미합니다. 하지만 이 값만으로 다음을 보장할 수는 없습니다.

  • 데이터베이스에 영구 저장되었는가?
  • 외부 시스템에 중복으로 반영되지 않았는가?
  • 비동기 후속 작업까지 끝났는가?
  • 같은 이벤트가 재전송되었을 때도 결과가 한 번만 적용되는가?

반대로 500이나 네트워크 타임아웃이 발생했다고 해서 아무 작업도 수행되지 않았다고 단정할 수도 없습니다. 응답 전달 단계에서 연결이 끊겼을 가능성이 있기 때문입니다.

웹훅은 왜 중복되어 들어오는가?

간단한 시나리오를 생각해 봅시다.

  1. 결제 서비스가 이벤트 evt_001을 보낸다.
  2. 수신 서버가 이벤트를 저장한다.
  3. 수신 서버의 응답이 전송자에게 도착하기 전에 연결이 끊긴다.
  4. 전송자는 실패로 판단하고 동일 이벤트를 다시 보낸다.
  5. 수신 서버에 중복 방어가 없다면 같은 작업을 또 실행한다.

이 문제를 줄이려면 이벤트의 고유 식별자와 Idempotency(멱등성) 정책을 설계해야 합니다. 멱등성은 동일한 작업을 여러 차례 요청해도 허용되는 부작용이 한 번만 발생하도록 하는 속성입니다.

Python + SQLite로 중복 이벤트를 재현해 보자

아래는 외부 웹훅을 받지 않고 동일한 이벤트를 두 차례 처리하는 메모리 기반 교육용 시뮬레이션입니다. SQLite의 PRIMARY KEY 중복 제한을 활용합니다.

import sqlite3

def accept_event(conn, event_id, payload):
    try:
        with conn:
            conn.execute(
                'INSERT INTO processed(event_id, payload) VALUES (?, ?)',
                (event_id, payload),
            )
    except sqlite3.IntegrityError:
        return 200, 'duplicate ignored'
    return 200, 'accepted'

conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE processed (event_id TEXT PRIMARY KEY, payload TEXT NOT NULL)')
print(accept_event(conn, 'evt_001', '{"kind":"updated"}'))
print(accept_event(conn, 'evt_001', '{"kind":"updated"}'))
print(conn.execute('SELECT COUNT(*) FROM processed').fetchone()[0])

실행 결과는 다음과 같습니다.

(200, 'accepted')
(200, 'duplicate ignored')
1

두 번 호출했지만 저장된 레코드는 하나입니다.
다만 이 예제에는 서명 검증, 실제 HTTP 처리, 영속적인 운영 데이터베이스, 외부 시스템 부작용의 원자적 처리가 포함되어 있지 않습니다. :memory: 데이터베이스는 프로세스 종료 후 사라지며, 실제 시스템에서는 재시작 뒤에도 유지되는 고유 키 저장소와 트랜잭션 또는 Outbox 패턴 같은 추가 설계가 필요합니다.


중복 방어만으로 웹훅이 안전해지지는 않는다

실패 조건확인할 사항
동일 이벤트 두 번 수신고유 이벤트 ID와 중복 처리 정책
전송자 타임아웃서버가 이미 처리했는지 재현 가능한 로그
일시적 5xx 오류제한된 횟수·간격의 재시도와 백오프
영구적인 요청 오류무제한 재시도 대신 오류 분류와 조사 경로
서명 불일치처리 전에 인증 검증 및 거부
처리 순서 변경이벤트 버전·상태 전이 및 순서 의존성 검토

운영 중인 이벤트 시스템은 전송 플랫폼의 재시도 규칙부터 확인해야 합니다.
어떤 서비스는 같은 이벤트를 여러 번 전달하며, 이벤트 순서도 보장하지 않을 수 있습니다.

예를 들어 AWS Lambda의 비동기 호출 문서 역시 중복 이벤트에 안전하게 대응하도록 권고합니다.

특히 외부 결제나 재고 변경이 이미 일어난 뒤 이벤트 ID만 기록하면 두 단계 사이의 장애에서 중복 부작용이 여전히 발생할 수 있으며, 단일 DB 트랜잭션, Outbox, downstream 고유 키 등을 고려해야 합니다.


실제 장애를 분석할 때 필요한 자료

웹훅 장애를 의뢰하거나 자체적으로 검증할 때는 다음 자료를 먼저 확보하는 것이 좋습니다.

  • 이벤트 식별자와 발생 시각(시간대 포함)
  • 전송자 측 재시도 기록과 수신 서버 응답 코드
  • 중복 실행으로 의심되는 부작용(레코드, 이메일, 주문 등)
  • 서명 검증 방식 및 비밀키 관리 경계(비밀값 자체는 공유하지 않음)
  • 정상·중복·일시 실패·영구 실패에 대한 테스트 기대값

DeepLinK는 합성 이벤트로 여러 실패 경로를 체험할 수 있는 Webhook Reliability Lab을 운영하고 있습니다. 이 도구는 브라우저 시뮬레이션이지 실제 외부 웹훅을 수신하고 장애를 복구하는 고객 사례가 아닙니다.

실제 시스템의 원인을 진단할 필요가 있다면 API Integration & Webhook Reliability Audit에서 작업 범위와 검수 조건을 확인할 수 있으며, 진단과 운영 코드 수정·배포는 별도 합의가 필요합니다.

기술 범위 문의: DEEPLINK Technical scope request 

Leave a Reply

Your email address will not be published. Required fields are marked *