시냅스

Road to Web3 (24) 멱등성 상태머신: 결제 중복 방지로 오프체인 버그를 구조적으로 막기 본문

Road To Web3/Blockchain

Road to Web3 (24) 멱등성 상태머신: 결제 중복 방지로 오프체인 버그를 구조적으로 막기

ted k 2026. 1. 4. 17:11
이 글은 EVM 계열을 기준으로 설명합니다.

 

하지만 핵심은 체인이 아니라 오프체인 시스템 설계입니다.
온체인 결제를 붙이는 순간, 결제 서비스는 분산 시스템이 됩니다.
그리고 분산 시스템은 중복과 역전과 재시도와 지연을 기본 옵션으로 제공합니다.

 

0) 요약

  • 온체인은 최종 원장이고, 오프체인은 제품 상태 머신이다
  • 중복과 역전과 reorg까지 고려하려면 멱등성과 상태 전이를 강하게 설계해야 한다

 

1) 제가 처음 겪은 오프체인 착각 3가지

제가 처음 web3를 접할 때 했던 오해는 이렇습니다.

  • tx hash가 있으면 결제는 끝났다고 생각했다
  • 이벤트 로그를 한 번 읽으면 그게 진실이라고 생각했다
  • 실패하면 다시 보내면 된다고 생각했다

현실은

  • tx hash는 접수 번호에 가깝고
  • 이벤트는 되돌릴 수 있으며
  • 재시도는 비용과 부작용이 있다

입니다.


 

2) 멱등성: 같은 요청이 여러 번 와도 결과는 한 번만

web2 결제에서 멱등성 키를 쓰는 이유는 명확합니다.

  • 네트워크 타임아웃
  • 재시도
  • PG 웹훅 중복
  • 서버 재시작

온체인 결제에서는 여기에 더해

  • RPC 재전송
  • 멤풀 replace
  • 체인 reorg
  • 인덱서 재색인 backfill

이 추가됩니다.

즉, 멱등성 키는 선택이 아니라 생존 장비입니다.

2.1 멱등성 키는 무엇으로 잡나

실무에서는 보통 2단계를 분리합니다.

  • 외부 요청 키: client request id
  • 결제 증빙 키: chain id + tx hash 또는 message id

한 줄로 말하면

  • 사용자의 의도 식별자와 체인의 증빙 식별자를 분리해서 둘 다 멱등 처리한다

입니다.

참고
web2에서와 마찬가지로 멱등키는 외부에서 유출이 어렵게 설계하는 편이 안전합니다. client request id는 UUIDv4 같은 충분히 랜덤한 값으로 만들면 현실적으로 추측이 어렵습니다. 다만 트랜잭션 해시 tx hash는 추측은 어렵지만, 네트워크에 전파된 이후에는 노드나 인덱서가 관측할 수 있는 공개 정보가 될 수 있습니다. 그래서 멱등키는 중복 처리 방지용 식별자로 쓰고, 인증이나 권한 부여 목적의 비밀 토큰처럼 쓰지는 않는 것이 좋습니다.


 

3) 상태 머신: 결제와 제공을 한 문장으로 묶지 말기

web2 결제에서 가장 위험한 문장은 이것입니다.

  • 결제가 되면 제공한다

온체인에서는 결제가 된다의 의미가 여러 층이기 때문입니다.

  • submitted: 브로드캐스트 성공
  • pending: 멤풀 대기
  • included: 블록 포함
  • confirmed: N 블록 쌓임
  • final: 되돌리기 어려움 또는 최종성 기준 충족

따라서 제공 정책도 상태 기반이어야 합니다.

  • 포함 시에 미리 제공할 것인가
  • 확인 후에 제공할 것인가
  • 최종성 이후에 제공할 것인가


 

4) 오프체인에서 터지는 실전 버그 6가지와 처방

4.1 중복 처리

상황

  • 같은 tx를 인덱서가 두 번 전달
  • 서비스가 두 번 제공

처방

  • fulfillment는 반드시 단일 테이블에서 원자적 upsert로 처리
  • 제공은 payment id 기준으로 유니크 제약을 둔다

4.2 누락

상황

  • 특정 블록 범위를 읽다가 장애로 구멍이 생김
  • 고객은 결제했는데 제공이 안 됨

처방

  • 커서 기반 소비 cursor와 주기적 backfill
  • 지표로 gap을 감지

4.3 reorg로 인한 취소

상황

  • included로 보고 제공했는데, reorg로 tx가 사라짐

처방

  • confirmation buffer를 둔다
  • 빠른 UX가 필요하면 부분 제공과 사후 회수 정책을 문장으로 고정한다

4.4 순서 역전

상황

  • 이벤트는 블록 순서로 오지만, 처리 큐에서 역전됨
  • 상태가 되돌아가거나 중간 상태가 덮임

처방

  • 상태 전이는 단조 증가 monotonic 해야 한다
  • 상태 업데이트는 이전 상태보다 낮으면 무시

4.5 재시도 폭주

상황

  • pending이 길어지자 자동 재시도가 폭주
  • 같은 nonce 또는 같은 메시지가 난사
  • 비용과 지연이 커짐

처방

  • 재시도는 큐 기반으로 제한
  • 실패 원인별 정책
  • 사용자 의도 없는 자동 재시도는 제한

4.6 환불과 제공의 레이스

상황

  • 고객지원이 환불을 처리하는 동안 제공이 진행
  • 또는 반대로 제공 후 환불이 자동 처리

처방

  • 결제와 제공, 환불을 하나의 상태 머신으로 합친다
  • 승인, 제공, 환불은 각각의 전이 조건을 가진다

 

5) web2 결제와 비교: 공통점과 차이점

공통점

  • 멱등성 키는 필수다
  • 상태 머신은 필수다
  • 웹훅과 폴링, 재시도는 기본 실패 모델이다

차이점

  • 차지백이나 네트워크 차원의 강제 롤백이 기본으로 없다
  • 실패해도 프로토콜 수수료가 나갈 수 있다
  • 확정은 하나가 아니라 단계다
  • 인덱싱 대상이 분산되어 있고, 재색인이 자연스러운 운영 행위다

 

6) 실전 구현 스케치: Spring 개발자 관점

핵심 테이블 3개만 있으면 구조가 잡힙니다.

1) payment_intent

  • client_request_id unique
  • amount, currency, customer
  • status: created, awaiting_proof, fulfilled, cancelled

2) payment_proof

  • chain_id + tx_hash unique
  • block_number, log_index
  • status: seen, included, confirmed, final, orphaned

3) fulfillment

  • payment_intent_id unique
  • status: ready, done, reversed
  • delivered_at, reversed_at

처리 루프

  • 인덱서가 proof를 upsert
  • 상태 전이를 단조 증가로 업데이트
  • fulfillment는 정책에 맞는 상태에서만 전이


 

7) 결론

  • 온체인 결제를 붙이면 오프체인에서 중복과 역전과 되돌림을 관리해야 한다
  • 멱등성 키는 외부 의도와 체인 증빙을 분리해 두 겹으로 잡는 편이 안전하다
  • 상태 머신은 결제와 제공, 환불을 하나로 묶어야 실전 버그를 막는다
Comments