Cheondi
개발 · 보안

중복 콜백과 멱등성

외부 결제 콜백의 중복·순서 변경·재시도를 멱등성과 상태 전이로 다룬 기록입니다.

  • #callback
  • #idempotency
  • #payment

외부 결제 시스템은 응답을 받지 못하면 같은 콜백을 다시 보낼 수 있다. 처음에는 콜백 API가 200을 반환하면 한 번만 올 거라고 막연히 생각했다. 실제로 네트워크 타임아웃, 공급자 재시도, 서버 재시작이 있으면 동일한 성공 알림이 여러 번 도착할 수 있었다.

중복 요청이 와도 거래 효과는 한 번만 발생하도록 만들어야 했다.

공급자 참조값과 콜백 이벤트 식별

서명 검증을 통과한 뒤 내부 거래 ID, 공급자 거래 참조, 이벤트 종류를 기준으로 처리 기록을 찾았다.

idempotency key = provider + providerReference + eventType

요청 본문 전체의 hash는 보조 증거로 사용할 수 있지만 필드 순서나 재전송 시각이 달라질 수 있어 계약상 안정적인 식별자를 우선했다.

확인과 갱신의 원자적 처리

“처리 기록 조회 → 없으면 상태 변경 → 기록 저장”을 따로 실행하면 동시에 온 두 요청이 모두 없다고 판단할 수 있었다. 데이터베이스의 unique 제약과 트랜잭션, 조건부 갱신을 사용해 한 요청만 전이를 소유하게 했다.

UPDATE payment_example
SET status = 'completed'
WHERE id = :id AND status = 'processing';

변경 행이 0이면 이미 처리됐거나 허용되지 않은 상태이므로 현재 값을 읽어 안전하게 응답했다. 실제 테이블과 상태명은 예시로 바꿨다.

지연 콜백의 과거 상태 전이 차단

성공 콜백 뒤 지연된 pending 이벤트가 도착할 수 있었다. 공급자 상태를 받은 순서대로 덮지 않고 허용된 내부 상태 전이표를 사용했다.

서명 실패는 처리 기록에 민감한 원문을 남기지 않고 거부했고, 알 수 없는 거래 참조는 정상 거래와 분리해 운영 확인 대상으로 보냈다.

응답을 보내기 직전에 서버가 종료되는 조건도 시험했다. 내부 거래는 완료됐지만 공급자는 응답을 받지 못해 다시 콜백을 보낼 수 있다. 두 번째 요청이 현재 상태를 읽고 같은 성공 응답을 안전하게 돌려주는지 확인했다.

서버 상태 보장으로서의 멱등성

중복 콜백에 “이미 처리됨”을 반환하는 것만으로는 부족했다. 잔액 반영, 알림 발송, 운영 기록 같은 부수 효과도 한 번이어야 했다. 트랜잭션 뒤 이벤트를 발행한다면 outbox 같은 전달 보장 구조도 검토했다.

이 작업 뒤 외부 시스템 요청은 한 번만 온다고 가정하지 않는다. 중복, 순서 변경, 부분 실패를 정상 조건으로 두고 상태와 부수 효과를 설계한다. 멱등성은 방어 코드 한 줄이 아니라 거래 경계 전체의 성질이었다.

공식 문서 기반의 추가 정리

나중에 같은 증상을 다시 만나지 않으려고 문서에 적힌 기준으로 구현을 한 번 더 정리했다.

RFC 9110은 HTTP 메서드의 의미와 멱등성을 구분한다. 같은 요청을 다시 보내도 의도한 서버 효과가 한 번과 같아야 멱등하다고 말할 수 있다. 클라이언트에서 버튼을 잠그는 것은 중복 조작을 줄이지만, 타임아웃 뒤 재전송이나 다른 기기 요청까지 막아 주지는 않는다.

또 상태 코드만으로 화면 상태를 모두 결정하기보다 응답이 어떤 요청과 선택 조건에 대한 것인지 확인해야 한다. 네트워크 성공과 현재 화면에 적용 가능한 응답은 다른 판단이다.

다음에 중복 콜백과 멱등성 같은 문제를 보면 아래 순서부터 확인하려고 한다.

  • 클라이언트 요청 ID와 공급자 참조 ID
  • 같은 콜백을 두 번 받은 결과
  • 타임아웃 뒤 조회·재시도 순서
  • 상태 전이와 원문 응답 보존

재시도는 횟수보다 같은 요청을 식별하는 키와 서버의 중복 처리 규칙이 먼저 정해져야 한다.

공식 참고 자료