안전한 청구 업데이트를 위한 멱등성 결제 웹훅 체크리스트
이 체크리스트는 이벤트 중복 제거, 재시도 처리, 인보이스·구독·권한을 안전하게 업데이트하는 방법을 안내합니다.

결제 웹훅이 중복 업데이트를 만드는 이유
결제 웹훅은 결제 제공자가 결제 성공, 인보이스 결제, 구독 갱신, 환불 등 중요한 일이 발생했을 때 백엔드로 보내는 메시지입니다. 요약하면 제공자가 “이런 일이 발생했습니다. 기록을 업데이트하세요.”라고 알려주는 셈입니다.
중복은 웹훅 전달이 정확히 한 번을 보장하도록 설계된 것이 아니라 신뢰성을 우선하기 때문에 발생합니다. 서버가 느리거나 타임아웃이 나거나 오류를 반환하거나 잠깐 사용 불가 상태면 제공자는 보통 같은 이벤트를 다시 보냅니다. 또한 하나의 실제 동작에 대해 서로 다른 이벤트(예: 하나의 결제에 연결된 인보이스 이벤트와 결제 이벤트)가 생길 수 있고, 환불처럼 빠르게 이어지는 후속 처리 때문에 이벤트가 순서대로 도착하지 않을 수도 있습니다.
핸들러가 멱등성이 없다면 같은 이벤트를 두 번 적용해 고객과 재무팀이 바로 알아차리는 문제가 생깁니다:
- 인보이스가 두 번 결제됨으로 표시되어 중복 회계 항목 발생
- 갱신이 두 번 적용되어 접근 기간이 과도하게 연장됨
- 권한(추가 크레딧, 좌석, 기능)이 두 번 부여됨
- 환불이나 차지백이 접근을 올바르게 되돌리지 못함
이건 단순한 “모범 사례” 문제가 아닙니다. 신뢰할 수 있는 청구인지, 문의 티켓을 만드는 청구인지의 차이입니다.
이 체크리스트의 목표는 간단합니다: 들어오는 각 이벤트를 "최대 한 번 적용"으로 처리하세요. 모든 이벤트에 대해 안정적인 식별자를 저장하고 재시도를 안전하게 처리하며 인보이스, 구독, 권한을 통제된 방식으로 업데이트합니다. AppMaster 같은 노코드 도구로 백엔드를 만든다면 동일한 규칙이 적용됩니다: 명확한 데이터 모델과 재시도 상황에서도 올바르게 유지되는 반복 가능한 핸들러 플로우가 필요합니다.
웹훅에 적용할 수 있는 멱등성 기본 원칙
멱등성은 같은 입력을 여러 번 처리해도 최종 상태가 같게 되는 것을 의미합니다. 청구 관점에서는: 하나의 인보이스는 한 번만 결제 처리되고, 하나의 구독은 한 번만 업데이트되며, 접근 권한은 한 번만 부여되어야 합니다. 웹훅이 두 번 전달되더라도 말입니다.
제공자는 엔드포인트가 타임아웃되거나 5xx를 반환하거나 네트워크가 끊기면 재시도합니다. 이러한 재시도는 같은 이벤트를 반복합니다. 며칠 후 발생하는 환불 같은 실제 변경을 나타내는 별개의 이벤트는 ID가 다릅니다.
이것을 작동시키려면 두 가지가 필요합니다: 안정적인 식별자와 이미 본 것을 기억할 작은 "메모리"입니다.
어떤 ID를 저장해야 하나(그리고 무엇을 보관할지)
대부분의 결제 플랫폼은 웹훅 이벤트에 고유한 이벤트 ID를 포함합니다. 일부는 요청 ID, 멱등성 키 또는 페이먼트 객체 ID(예: charge 또는 payment intent) 같은 고유 식별자를 페이로드 안에 제공합니다.
다음 질문에 답할 수 있게 저장하세요: “이 정확한 이벤트를 이미 적용했나?”
실무적 최소 항목:
- 이벤트 ID(고유 키)
- 이벤트 타입(디버깅에 유용)
- 수신 타임스탬프
- 처리 상태(처리됨/실패)
- 영향을 받은 고객, 인보이스 또는 구독에 대한 참조
핵심은 이벤트 ID를 고유 제약이 있는 테이블에 저장하는 것입니다. 그런 다음 핸들러는 안전하게 이렇게 할 수 있습니다: 먼저 이벤트 ID를 삽입하세요; 이미 존재하면 중복으로 보고 중지하고 200을 반환합니다.
중복 기록은 얼마나 오래 보관해야 하나
지연된 재시도와 조사 기간을 커버할 만큼 중복 기록을 보관하세요. 일반적인 창(window)은 3090일입니다. 차지백, 분쟁 또는 긴 구독 주기를 다루면 더 오래(612개월) 보관하고 오래된 행을 정리해 테이블이 빠르게 유지되도록 하세요.
AppMaster 같은 생성된 백엔드에서는 이 것이 WebhookEvents 모델에 이벤트 ID에 대한 유니크 필드를 두고, 중복이 감지되면 조기 종료하는 비즈니스 프로세스로 깔끔하게 매핑됩니다.
이벤트 중복 제거를 위한 간단한 데이터 모델 설계
좋은 웹훅 핸들러는 대부분 데이터 문제입니다. 제공자 이벤트를 정확히 한 번씩 기록할 수 있다면 그 다음의 모든 처리가 더 안전해집니다.
영수증 로그처럼 동작하는 하나의 테이블로 시작하세요. PostgreSQL(또는 AppMaster의 Data Designer에 모델링할 때)에서는 작고 엄격하게 유지해 중복이 빠르게 실패하도록 하세요.
최소로 필요한 항목
실무적인 기준의 webhook_events 테이블 예:
provider(텍스트, 예: "stripe")provider_event_id(텍스트, 필수)status(텍스트, 예: "received", "processed", "failed")processed_at(타임스탬프, nullable)raw_payload(jsonb 또는 text)
(provider, provider_event_id)에 유니크 제약을 추가하세요. 이 단일 규칙이 주요한 중복 방지 수단입니다.
업데이트할 레코드를 찾기 위해 사용하는 비즈니스 ID도 필요합니다. 이는 웹훅 이벤트 ID와 다릅니다.
일반적으로 customer_id, invoice_id, subscription_id 같은 값들을 텍스트로 보관하세요. 제공자들이 종종 숫자가 아닌 ID를 사용하기 때문입니다.
원시 페이로드 vs 파싱된 필드
원시 페이로드를 저장하면 디버깅과 재처리 시 유용합니다. 파싱된 필드는 쿼리와 리포팅을 쉽게 하지만 실제로 사용하는 것만 저장하세요.
간단한 접근법:
- 항상
raw_payload를 저장하세요 - 자주 조회하는 몇몇 파싱된 ID(고객, 인보이스, 구독)도 저장하세요
- 필터링을 위한 정규화된
event_type(텍스트)을 저장하세요
예를 들어 invoice.paid 이벤트가 두 번 도착하면 유니크 제약이 두 번째 삽입을 막습니다. 감사용으로 원시 페이로드가 남아 있고, 파싱된 인보이스 ID로 첫 번째 업데이트 시점을 쉽게 찾을 수 있습니다.
단계별: 안전한 웹훅 핸들러 플로우
안전한 핸들러는 일부러 지루합니다. 제공자가 같은 이벤트를 재시도하거나 이벤트가 순서대로 오지 않아도 항상 같은 방식으로 동작합니다.
매번 따라야 할 5단계 플로우
-
서명을 검증하고 페이로드를 파싱하세요. 서명 체크에 실패하거나, 예상치 못한 이벤트 타입이거나, 파싱할 수 없으면 요청을 거부하세요.
-
청구 데이터를 건드리기 전에 이벤트 레코드를 기록하세요. 제공자 이벤트 ID, 타입, 생성 시각, 원시 페이로드(또는 해시)를 저장하세요. 이벤트 ID가 이미 존재하면 중복으로 처리하고 중단하세요.
-
이벤트를 단일 “소유자” 레코드에 매핑하세요. 무엇을 업데이트할지 결정하세요: 인보이스, 구독, 또는 고객. 외부 ID를 레코드에 저장해 직접 조회할 수 있게 하세요.
-
안전한 상태 변경을 적용하세요. 상태는 항상 앞으로만 이동시키세요. 늦게 도착한 "invoice.updated"로 이미 결제된 인보이스를 되돌리지 마세요. 적용한 내용(이전 상태, 새 상태, 타임스탬프, 이벤트 ID)을 감사용으로 기록하세요.
-
빠르게 응답하고 결과를 로깅하세요. 이벤트가 안전하게 저장되고 처리되었거나 무시되면 성공을 반환하세요. 처리되었는지, 중복으로 무시되었는지, 거부되었는지와 그 이유를 로깅하세요.
AppMaster에서는 보통 웹훅 이벤트용 데이터베이스 테이블과 "이벤트 ID를 이미 보았는가?"를 확인한 뒤 최소한의 업데이트 단계를 실행하는 비즈니스 프로세스로 매핑됩니다.
재시도, 타임아웃, 순서가 뒤바뀐 전달 처리
제공자는 빠른 성공 응답을 받지 못하면 웹훅을 재시도합니다. 또한 이벤트를 순서대로 보내지 않을 수 있습니다. 동일한 업데이트가 두 번 도착하거나 나중 이벤트가 먼저 도착해도 핸들러는 안전해야 합니다.
실무 규칙 하나: 빠르게 응답하고 작업은 나중에 하세요. 웹훅 요청을 영수증으로 취급하고 무거운 로직을 그 안에서 실행하지 마세요. 타사 API 호출, PDF 생성, 계정 재계산 등을 요청 처리 안에서 하면 타임아웃이 늘어나 재시도가 더 빈번해집니다.
순서가 뒤바뀌었을 때: 최신 상태를 유지하세요
순서가 뒤바뀌는 것은 정상입니다. 변경을 적용하기 전에 두 가지 검사를 하세요:
- 타임스탬프 비교: 객체(인보이스, 구독, 권한)에 대해 이미 저장된 것보다 최신인 경우만 적용하세요.
- 타임스탬프가 근접하거나 불명확할 때는 상태 우선순위를 사용하세요: paid는 open을 덮고, canceled는 active를 덮습니다.
이미 인보이스를 결제로 기록해두고 늦게 "open" 이벤트가 도착하면 무시하세요. "canceled"를 받았는데 이후 더 오래된 "active" 업데이트가 나타나면 취소된 상태를 유지하세요.
무시 vs 큐잉
오래되었거나 이미 적용되었음을 증명할 수 있으면 이벤트를 무시하세요(같은 이벤트 ID, 더 오래된 타임스탬프, 낮은 상태 우선순위). 반대로 필요한 데이터가 아직 없어 처리할 수 없는 경우(예: 고객 레코드가 아직 없음) 이벤트를 큐에 넣으세요.
실무 패턴:
- 이벤트를 즉시 저장하고 처리 상태(received, processing, done, failed)를 두세요
- 종속 데이터가 없으면 waiting으로 표시하고 백그라운드에서 재시도하세요
- 재시도 제한을 두고 반복 실패 시 알림을 설정하세요
AppMaster에서는 웹훅 이벤트 테이블과 요청을 빠르게 확인하고 큐에 넣은 이벤트를 비동기적으로 처리하는 비즈니스 프로세스가 잘 맞습니다.
인보이스, 구독, 권한을 안전하게 업데이트하기
중복 제거를 처리한 다음의 위험은 분리된 청구 상태입니다: 인보이스는 결제됨으로 표시되었지만 구독은 여전히 연체 상태이거나, 접근이 두 번 부여되어 제대로 회수되지 않는 경우 등이 있습니다. 모든 웹훅을 상태 전이로 취급하고 한 번에 원자적으로 적용하세요.
인보이스: 상태 변경을 단조롭게(모노토닉) 유지하세요
인보이스는 paid, voided, refunded 같은 상태를 거칩니다. 부분 결제가 있을 수도 있습니다. 어떤 이벤트가 마지막으로 도착했는지에 따라 인보이스를 토글하지 마세요. 현재 상태와 주요 총액(amount_paid, amount_refunded)을 저장하고 앞으로 안전한 전이만 허용하세요.
실무 규칙:
- 인보이스는 한 번만 paid로 표시하세요. paid 이벤트를 처음 보았을 때만 표시합니다.
- 환불이 발생하면
amount_refunded를 인보이스 총액까지 증가시키고 절대 감소시키지 마세요. - 인보이스가 voided되면 이행(fulfillment) 동작을 중지하되 감사용으로 레코드를 유지하세요.
- 부분 결제의 경우 "완전 결제(fully paid)" 이점을 주지 말고 금액만 업데이트하세요.
구독과 권한: 부여는 한 번, 회수는 한 번
구독에는 갱신, 취소, 유예 기간이 포함됩니다. 구독 상태와 기간 경계(current_period_start/end)를 유지하고 권한 기간은 그 데이터에서 유도하세요. 권한은 단순한 불리언이 아니라 명시적 레코드로 관리하세요.
접근 제어를 위한 규칙:
- 사용자별/상품별/기간별로 권한 부여는 한 번만
- 접근이 종료될 때(취소, 환불, 차지백) 한 번의 회수 기록을 남김
- 어떤 웹훅 이벤트가 각 변경을 발생시켰는지 감사 로그로 기록
분리 상태를 피하려면 한 트랜잭션에서 처리하세요
인보이스, 구독, 권한 업데이트를 하나의 데이터베이스 트랜잭션에서 적용하세요. 현재 행을 읽고, 이 이벤트가 이미 적용되었는지 확인한 뒤 모든 변경을 함께 기록하세요. 실패하면 롤백되도록 하세요. 그래야 "인보이스는 결제됨"인데 "접근 없음" 같은 상태 불일치가 생기지 않습니다.
AppMaster에서는 보통 하나의 Business Process 흐름에서 PostgreSQL을 제어하고 비즈니스 변경과 함께 감사 항목을 작성하는 방식으로 잘 매핑됩니다.
웹훅 엔드포인트 보안 및 데이터 안전 검사
웹훅 보안은 정확성의 일부입니다. 공격자가 엔드포인트에 접근할 수 있다면 위조된 "결제 완료" 상태를 만들 수 있습니다. 중복 제거가 있어도 이벤트가 진짜인지 증명하고 고객 데이터를 안전하게 지켜야 합니다.
청구 데이터를 건드리기 전에 발신자를 검증하세요
모든 요청의 서명을 검증하세요. Stripe의 경우 일반적으로 Stripe-Signature 헤더를 확인하고 원시 요청 본문(raw request body)을 사용하며(재작성된 JSON이 아님) 오래된 타임스탬프는 거부합니다. 헤더가 없으면 바로 실패 처리하세요.
초기 검증으로는 올바른 HTTP 메서드, Content-Type, 필요한 필드(이벤트 id, 타입, 인보이스나 구독을 찾는 데 쓸 객체 id)를 확인하세요. AppMaster로 빌드하면 서명 비밀은 환경 변수나 보안 설정에 두고 데이터베이스나 클라이언트 코드에 두지 마세요.
빠른 보안 체크리스트:
- 유효한 서명과 최신 타임스탬프가 없으면 요청을 거부
- 예상 헤더와 콘텐츠 타입을 요구
- 웹훅 핸들러는 최소 권한의 DB 접근만 사용
- 비밀값은 테이블 밖(env/config)에 저장하고 필요 시 교체
- 이벤트를 안전하게 영구 저장한 뒤에만 2xx 응답 반환
비밀 유출 없이 로그를 유용하게 유지하세요
재시도와 분쟁을 디버깅할 수 있을 만큼 충분히 로그를 남기되 민감한 값은 피하세요. 저장할 수 있는 안전한 PII의 부분집합: 제공자 고객 ID, 내부 사용자 ID, 마스킹된 이메일(예: a***@domain.com). 전체 카드 데이터, 전체 주소, 원시 인증 헤더는 절대 저장하지 마세요.
재구성에 도움이 되는 로그 항목:
- 제공자 이벤트 id, 타입, 생성 시각
- 검증 결과(서명 정상/실패) — 단, 서명 자체는 저장 금지
- 중복 결정(new vs already processed)
- 수정된 내부 레코드 ID(인보이스/구독/권한)
- 오류 이유와 재시도 횟수(큐를 사용하는 경우)
기본적인 남용 방지도 추가하세요: IP별 속도 제한과(가능하면) 고객 ID별 제한, 필요 시 제공자 IP 범위만 허용하는 것도 고려하세요.
중복 청구나 중복 접근을 발생시키는 흔한 실수
대부분의 청구 버그는 수학 문제 때문이 아닙니다. 웹훅 전달을 단일 신뢰 가능한 메시지로 취급할 때 발생합니다.
자주 발생하는 실수:
- 타임스탬프나 금액으로 중복 제거: 서로 다른 이벤트가 같은 금액을 가질 수 있고 재시도가 몇 분 후에 도착할 수 있습니다. 제공자의 고유 이벤트 ID를 사용하세요.
- 서명 검증 전에 DB를 업데이트: 먼저 검증하고, 파싱한 뒤에 행동하세요.
- 현재 상태를 확인하지 않고 모든 이벤트를 신뢰: 이미 결제되거나 환불된 인보이스를 맹목적으로 paid로 표기하지 마세요.
- 동일 구매에 대해 여러 권한 생성: 재시도로 중복 행이 생깁니다.
ensure entitlement exists for subscription_id같은 upsert 규칙을 선호하세요. - 알림 서비스가 다운되어 웹훅을 실패 처리함: 이메일, SMS, Slack 등은 청구를 차단하면 안 됩니다. 알림은 큐에 넣고 핵심 청구 변경을 안전하게 저장한 뒤 성공을 반환하세요.
간단한 예: 갱신 이벤트가 두 번 도착했습니다. 처음 전달은 권한 행을 생성합니다. 재시도가 두 번째 행을 생성해 앱이 "두 개의 활성 권한"이라고 판단하면 추가 좌석이나 크레딧을 부여하게 됩니다.
AppMaster에서는 수정 방법이 대부분 흐름에 관한 것입니다: 먼저 검증, 유니크 제약으로 이벤트 레코드 삽입, 상태 검사와 함께 청구 업데이트 적용, 부수 효과(이메일, 영수증)는 비동기 단계로 밀어 재시도 폭주를 막으세요.
실무 예시: 중복 갱신 + 이후 환불
이 패턴은 겁나 보일 수 있지만 핸들러가 다시 실행해도 안전하도록 설계되어 있다면 관리 가능합니다.
고객이 월간 요금제를 사용 중입니다. Stripe가 갱신 이벤트(예: invoice.paid)를 보냈습니다. 서버가 이를 수신해 DB를 업데이트했지만 200 응답을 반환하는 데 너무 오래 걸렸습니다(콜드 스타트, 바쁜 DB 등). Stripe는 실패한 것으로 보고 같은 이벤트를 재시도합니다.
첫 전달에서 접근을 부여합니다. 재시도에서는 같은 이벤트임을 감지하고 아무 작업도 하지 않습니다. 이후 환불 이벤트(예: charge.refunded)가 도착하면 한 번만 접근을 회수합니다.
데이터베이스를 모델링하는 간단한 방법(앱마스터 Data Designer에서 만들 수 있는 테이블):
webhook_events(event_id UNIQUE, type, processed_at, status)invoices(invoice_id UNIQUE, subscription_id, status, paid_at, refunded_at)entitlements(customer_id, product, active, valid_until, source_invoice_id)
각 이벤트 후의 DB 상태 예시
Event A(갱신, 첫 전달) 후: webhook_events에 event_id=evt_123로 새 행이 추가되고 status=processed가 됩니다. invoices는 paid로 표시되고 entitlements.active=true, valid_until이 한 결제 주기만큼 앞으로 이동합니다.
Event A 재전달(재시도) 후: webhook_events 삽입이 유니크 제약으로 실패하거나 핸들러가 이미 처리된 것으로 보고 아무 변경도 하지 않습니다.
Event B(환불) 후: webhook_events에 event_id=evt_456 새 행이 추가됩니다. invoices.refunded_at이 설정되고 status=refunded가 됩니다. entitlements.active=false로 설정되거나 valid_until이 현재 시각으로 조정되어 source_invoice_id를 사용해 접근을 한 번만 회수합니다.
중요한 세부는 타이밍입니다: 중복 검사(듀프 체크)는 권한을 부여하거나 회수하기 전에 수행됩니다.
출시 전 빠른 체크리스트
실제 웹훅을 켜기 전에 하나의 실제 이벤트가 제공자가 열 번 보냈든 한 번만 적용되어 청구 레코드가 한 번만 업데이트되는 것을 증명해야 합니다.
엔드투엔드 설정을 검증하는 체크리스트:
- 들어오는 모든 이벤트는 먼저 저장되는가(원시 페이로드, 이벤트 id, 타입, 생성 시각, 서명 검증 결과 포함)? 이후 단계가 실패하더라도 저장되어야 합니다.
- 중복은 초기에 감지되어(같은 제공자 이벤트 id) 핸들러가 인보이스/구독/권한을 변경하지 않고 종료하는가?
- 비즈니스 업데이트가 단일 실행인지 증명할 수 있는가: 한 번의 인보이스 상태 변경, 한 번의 구독 상태 변경, 한 번의 권한 부여/회수.
- 실패는 재실행 가능하게 기록되는가(오류 메시지, 실패한 단계, 재시도 상태).
- 핸들러가 빠르게 응답하는가: 이벤트를 저장하면 바로 수신 확인을 하고 요청 내부에서 느린 작업을 피하는가?
대규모 관찰(오버시빌리티) 세트가 없어도 시작할 수 있지만 신호는 필요합니다. 로그나 단순 대시보드에서 다음을 추적하세요:
- 중복 전달 급증(정상 범위이지만 큰 증가 시 타임아웃이나 제공자 문제 신호)
- 이벤트 타입별 높은 오류율(예: invoice.payment_failed)
- 재시도에 갇힌 이벤트의 백로그 증가
- 불일치 체크(결제된 인보이스지만 권한 누락, 권한이 회수되었는데 접근이 남아 있음)
- 처리 시간의 급격한 증가
AppMaster로 구축한다면 이벤트 저장을 전용 테이블에 두고 "처리됨으로 표시"를 비즈니스 프로세스 내 단일 원자적 결정 지점으로 만드세요.
다음 단계: 테스트, 모니터링, 노코드 백엔드로 구축하기
테스트는 멱등성이 증명되는 곳입니다. 단순한 정상 경로만 테스트하지 마세요. 동일한 이벤트를 여러 번 재생(replay)하고, 이벤트를 순서 없이 보내고, 타임아웃을 강제해 제공자가 재시도하도록 만드세요. 두 번째, 세 번째, 열 번째 전달은 아무런 변경을 만들어내지 않아야 합니다.
초기에는 백필(backfilling) 계획을 세우세요. 버그 수정, 스키마 변경, 제공자 사고 후 과거 이벤트를 재처리해야 할 일이 생깁니다. 핸들러가 진정으로 멱등성이 있다면 백필은 “같은 파이프라인을 통해 이벤트 재생”하는 것만으로 중복을 만들지 않고 안전하게 재처리할 수 있습니다.
지원팀에는 간단한 런북을 제공해 문제를 추적하도록 하세요:
- 이벤트 ID를 찾아 이미 처리되었는지 확인
- 인보이스 또는 구독 레코드를 확인해 예상 상태와 타임스탬프 확인
- 권한 레코드(언제, 왜 접근이 부여되었는지) 검토
- 필요한 경우 단일 이벤트 ID에 대해 안전한 재처리 모드로 다시 실행
- 데이터 불일치가 있으면 단일 시정 조치를 적용하고 기록
많은 보일러플레이트 코드를 쓰지 않고 구현하려면 AppMaster(appmaster.io)이 핵심 테이블을 모델링하고 웹훅 플로우를 시각적 Business Process로 만드는 데 도움을 줍니다. 생성된 백엔드를 배포하기 전에 재시도 상황에서도 안전한지 반드시 확인하세요.
자주 묻는 질문
중복 웹훅 전달은 일반적입니다. 제공자가 최소 한 번 이상(at least once) 전달을 목표로 하기 때문에, 엔드포인트가 타임아웃되거나 5xx를 반환하거나 연결이 잠깐 끊기면 동일한 이벤트를 재전송합니다.
공급자가 제공하는 고유한 이벤트 ID(웹훅 이벤트 식별자)를 사용하세요. 금액, 타임스탬프, 고객 이메일로 중복 제거하지 마세요. 이벤트 ID를 유니크 제약으로 저장하면 재시도를 즉시 감지하고 안전하게 무시할 수 있습니다.
인보이스, 구독, 권한을 업데이트하기 전에 먼저 이벤트 레코드를 삽입하세요. 삽입이 실패해 이벤트 ID가 이미 존재하면 처리를 멈추고 성공을 반환해 재시도가 중복 업데이트를 만들지 않게 하세요.
지연된 재시도와 조사 기간을 커버할 만큼 보관하세요. 실무적으로는 30–90일이 일반적이고, 분쟁이나 차지백, 긴 구독 주기가 있다면 6–12개월까지 보관한 뒤 오래된 행은 정리해 테이블 성능을 유지하세요.
서명을 검증한 뒤에만 청구 데이터를 건드리세요. 서명 검증을 먼저 하지 않으면 위조된 “결제 완료” 이벤트로부터 보호받을 수 없습니다. 검증 실패 시 요청을 거부하고 청구 변경을 기록하지 마세요.
이벤트가 안전하게 저장된 후에 수신을 빠르게 확인하고 무거운 작업은 백그라운드로 옮기세요. 느린 핸들러는 타임아웃을 유발하고, 타임아웃은 재시도를 늘려 중복 업데이트 위험을 키웁니다.
상태를 앞으로만 이동시키고 오래된 이벤트는 무시하세요. 가능하면 이벤트 타임스탬프를 비교하고, 타임스탬프가 애매하면 상태 우선순위를 사용하세요(예: refunded는 paid를 덮어쓴다).
매 이벤트마다 새 권한 행을 만들지 마세요. “사용자/상품/기간(또는 구독) 당 하나의 권한을 보장”하는 upsert 규칙을 적용하고 날짜나 한도를 업데이트하세요. 어떤 이벤트 ID가 변경을 발생시켰는지 기록해 감사할 수 있게 하세요.
인보이스, 구독, 권한 변경을 하나의 데이터베이스 트랜잭션으로 작성하세요. 이렇게 하면 ‘인보이스는 결제됨’이지만 ‘접근이 부여되지 않음’ 같은 분리된 상태를 방지할 수 있습니다.
예 — 가능합니다. WebhookEvents 모델에 유니크 이벤트 ID를 만들고 “이미 보았는가?”를 확인하는 Business Process를 구축하세요. 인보이스/구독/권한을 Data Designer에 명시적으로 모델링하면 재시도와 재실행 시 중복 행 생성을 피할 수 있습니다.


