판단의 출발점

결제 사업자 화면에는 승인 내역이 있는데, 고객의 주문 목록은 비어 있습니다. 운영자가 주문을 다시 처리하자 이번에는 이용권이 두 장 발급됩니다. 설계 예시지만 어느 단계까지 성공했는지 모른 채 전체 과정을 다시 실행할 때 생길 수 있는 문제입니다.

이때 필요한 것은 결제 버튼을 다시 누르는 기능이 아닙니다. 주문이 무엇을 약속했는지, 결제가 어디까지 진행됐는지, 상품이나 권한을 실제로 제공했는지를 따로 기록하고 확인된 사실을 기준으로 빠진 단계만 처리하는 구조다. 외부 결제와 자체 데이터베이스를 한 번에 커밋할 수 있다고 가정하지 않고, 그 사이에서 끊긴 작업을 찾아 복구하도록 설계합니다.[9]

결제 결과를 확인하지 못했다면 실패가 아니라 미확정으로 남겨야 합니다. 결제는 성공했고 상품 제공만 실패했다면 다시 결제하지 않습니다. 상품 제공의 응답만 유실됐다면, 실제 제공 여부를 확인하기 전에는 새로 발급하지 않습니다. 이 구분이 안전한 재처리의 출발점입니다.

‘성공’이라는 상태 하나로는 부족하다

이 글은 토스페이먼츠의 결제창 인증 후 서버에서 승인하는 흐름을 주된 예로 사용합니다. 내부 데이터 모델은 PostgreSQL 17을 기준으로 설명하는 설계 예시이며 특정 상점의 구현이나 실제 장애 사례가 아닙니다. 정기 청구 정책, 부분 환불액 계산, 정산·은행 입금액 대사는 별도의 문제로 둡니다.

주문은 구매 내용과 이행 정책을, 결제 기록은 결제 사업자가 확인한 거래 사실을, 제공 기록은 재고 차감·이용권 발급·배송 요청 같은 실제 작업을 담당하게 합니다. 세 상태의 연결은 필요하지만 같은 값으로 덮어쓰지는 않습니다.

상황내부 주문 상태 예시토스 결제 상태 또는 확인 결과실제 제공 상태 예시다음 판단
결제창 인증 후 서버 승인 전결제 대기IN_PROGRESS미시작주문 검증 후 승인 절차 진행
가상계좌 발급 후 입금 전입금 대기WAITING_FOR_DEPOSIT미시작발급을 입금 완료로 취급하지 않음
결제 승인과 내부 반영 완료주문 확정DONE제공 대기제공 작업을 별도 실행
승인됐지만 주문 DB 반영 실패복구 필요재조회로 DONE 확인미시작결제하지 않고 내부 반영 복구
외부 이용권 발급 응답 유실주문 확정결제 확인 완료결과 미확정발급 결과 조회 후 다음 행동 결정
주문과 제공 모두 완료이행 완료결제 확인 완료제공 완료완료 증거 보존, 후속 정정은 별도 처리
결제 취소 확인전체·부분 취소 검토/반영CANCELED / PARTIAL_CANCELED대상 범위의 중단·회수 검토취소 대상과 이미 제공한 범위를 구분

위의 내부 상태명은 제안이며 PG의 상태값이 아닙니다. 토스의 DONE은 승인 완료를 뜻하지만 카드 매입 상태는 별도 필드이고 정산 정보도 구분됩니다. 이를 곧바로 ‘판매자 계좌에 입금 완료’로 해석하면 안 됩니다.[2]

상태에 숫자를 붙여 큰 값만 채택하는 방법도 안전하지 않습니다. 토스의 현행 웹훅 문서는 가상계좌 입금 오류에서 DONE이 WAITING_FOR_DEPOSIT으로 바뀔 수 있다고 설명합니다. 같은 문서는 1.4 이하 버전의 동작을 별도로 구분합니다. 오래된 알림에 의한 잘못된 되돌림을 막는 것과, 결제 사업자가 실제로 정정한 결과를 반영하는 것은 다른 일입니다.[4]

주문과 결제 시도는 같은 식별자가 아니다

다음 연결을 먼저 정해 두는 것을 권합니다.

내부 주문 → 결제 시도 → PG 주문 식별자·결제 식별자 → 상품 단위의 제공 작업

내부 주문에는 상품, 수량, 서버가 계산한 결제 예정 금액, 통화, 구매 주체, 적용 정책을 기록합니다. 결제 시도에는 어떤 PG·상점 계정·테스트 또는 운영 환경으로 어떤 요청을 보냈는지 남깁니다. 토스의 orderId와 paymentKey도 이 시도에 연결합니다.[1][2]

설계 예시: 한 내부 주문에서 실제로 새로운 결제 시도를 허용하면 별도의 시도 레코드와 PG 주문 식별자를 연결합니다. 그러나 응답 유실 뒤 같은 작업을 재시도하는 것은 새로운 결제 시도가 아닙니다. 기존 식별자와 작업 기록을 유지합니다. 이전 시도가 미확정인 동안 새 결제를 허용할지는 별도 정책으로 제한합니다.

반면 상품 제공의 중복 방지 기준은 결제 시도가 아니라 구매한 상품·권한의 단위다. 주문 하나에 결제가 두 번 성공했더라도, 이용권을 두 장 발급하는 것으로 오류를 덮어서는 안 됩니다. 두 결제 사실은 모두 보존하고 추가로 받은 결제는 별도 검토 대상으로 남깁니다.

서비스 작동 방식

주문에서 승인과 환불까지 상태를 연결 — 결제수단을 주문 흐름에 붙이고 응답 지연과 재시도, 취소와 환불의 예외를 검증합니다.

성공 화면이 아니라 서버가 주문을 확정한다

토스의 해당 흐름에서 성공 URL로 돌아오는 것은 구매자 인증 단계의 결과입니다. 서버의 결제 승인은 별도입니다. 공식 가이드는 결제 요청 전에 주문번호와 금액을 저장하고 승인 전에 돌아온 값과 비교하도록 설명합니다.[1]

구현할 때는 ‘클라이언트가 보낸 금액을 서버에 저장했으니 안전하다’고 끝내지 않습니다. 다음은 그 가이드에 더해 적용할 서버 검증 설계다.

승인 전에는 해당 주문에 접근할 권한이 있는지, 주문이 아직 결제 가능한지, 가격·할인·수량으로 서버가 계산한 금액과 요청 금액이 같은지 확인합니다. PG·상점 계정·환경·통화도 주문의 기대값에 묶습니다. 브라우저가 보낸 다른 주문번호로 처리 대상을 바꾸게 해서는 안 됩니다.

검증한 시도에 대해서만 POST /v1/payments/confirm을 호출하고 반환된 결제 식별자·주문번호·금액·상태를 기대값과 대조합니다. 결과를 잃었다면 paymentKey 또는 orderId를 이용한 공식 조회 경로로 확인합니다.[2]

결제 승인 응답이 가상계좌 발급 결과라면 바로 상품을 제공하는 분기로 보내지 않습니다. 화면도 서버의 주문·결제·제공 상태를 읽어서 ‘입금 대기’, ‘결제 확인 중’, ‘결제 완료·상품 준비 중’을 구분하는 편이 낫습니다. 브라우저가 닫히거나 성공 페이지를 다시 열어도 실제 처리 결과는 유지돼야 합니다.

멱등 키 하나가 모든 중복을 막아주지는 않는다

멱등성은 같은 논리적 작업을 반복 요청해도 결과를 중복해서 만들지 않도록 하는 성질입니다. 여기서는 어느 시스템의 어떤 작업을 같은 것으로 볼지가 중요합니다. 결제 사업자의 승인 중복 방지와 우리 시스템의 이용권 중복 발급 방지는 서로 다른 책임입니다.

결제 API에는 같은 작업의 키를 유지한다

토스는 Idempotency-Key와 API 키, API 주소, HTTP 메서드의 조합으로 같은 요청을 구분합니다. 문서상 유효기간은 최초 사용일부터 15일이며 첫 요청의 응답을 다시 돌려줍니다. 오류가 났다는 이유로 키를 바꿔 같은 요청을 반복하는 것은 위험하다고 안내합니다.[3]

이에 맞춘 설계는 승인 작업을 만들 때 키와 요청 내용을 영속적으로 저장하는 것입니다. 요청 금액·대상·작업 종류가 달라졌는데 같은 키로 보내려 하면 내부에서 차단합니다. 동일 작업의 재시도는 저장한 내용을 그대로 사용합니다.

키의 유효기간이 지났거나 인증 API 키를 교체했다고 해서 미확정 결제를 새로운 작업으로 실행하지 않습니다. 먼저 기존 거래를 확인해야 합니다. 또한 첫 응답을 다시 받는 것과 현재 결제 상태를 조회하는 것은 다릅니다. 승인 이후 취소가 있었을 가능성이 있다면, 재전송된 과거 승인 응답만 보고 주문을 다시 확정하지 않는 것이 안전합니다. 이는 위의 멱등 키 범위와 응답 재사용 방식에서 도출되는 설계상 주의점입니다.[3]

자체 DB에는 별도의 유일성 제약을 둔다

다음은 중복 방지 책임을 나눈 설계 예시입니다.

보호할 대상같은 작업으로 볼 기준 예시보호하지 못하는 것
PG 승인·취소 요청논리적 작업 ID, 요청 내용, PG가 정한 멱등 키 범위우리 DB 반영·상품 발급
내부 결제 레코드PG + 상점 계정 + 환경 + PG 결제 식별자결제의 이후 취소·정정 이벤트
웹훅 수신 기록공급자가 보장하는 이벤트 식별자와 범위다른 이벤트 ID로 표현된 동일한 업무 효과
내부 상품·권한 제공주문 상품 단위 + 제공 행위 + 필요한 이행 버전외부 발급 시스템에서 이미 실행된 작업
외부 상품 발급 요청발급처가 지원하는 고정 작업 식별자·멱등 키발급처의 중복 방지 범위를 벗어난 작업

이 예시는 상품 단위마다 한 번 제공하는 모델입니다. 여러 개를 구매하거나 분할 배송한다면 제공 단위를 먼저 나누어야 합니다. 정상적인 추가 구매까지 중복으로 제거해서도 안 됩니다.

PostgreSQL의 유일성 제약은 열 조합의 중복을 막는 데 사용할 수 있습니다. 다만 기본 동작에서 NULL은 서로 같은 값으로 취급되지 않으므로, 중복 방지 키가 비어 있어도 안전하다고 가정하면 안 됩니다. 키가 아직 없는 결제 시도와, 식별자가 확정된 결제 레코드를 구분하고 필요한 열에 NOT NULL 등을 적용합니다.[8]

애플리케이션에서 ‘없으면 생성’이라고 먼저 조회하는 것만으로는 동시 요청을 막기 어렵습니다. 유일성 제약과 상태 전이 조건을 함께 사용하고 충돌한 요청은 기존 처리 결과를 다시 읽도록 설계합니다. 결제 하나를 이미 봤다는 이유로 이후의 취소까지 무시하지 않도록, 결제 식별자와 개별 작업·이벤트 식별자는 구분합니다.

DB에 남긴 성공과 다음 작업을 함께 커밋한다

자체 데이터베이스 트랜잭션은 그 안의 변경들을 함께 확정하거나 취소하는 경계입니다. 외부 PG의 승인까지 자동으로 되돌리는 경계는 아닙니다.[7][9]

다음은 로컬 트랜잭션과 외부 호출을 분리한 설계 예시다.

  1. 호출 전: 주문과 결제 시도, 승인 요청 내용과 멱등 키를 저장합니다. 외부 결제 호출을 기다리는 동안 주문 행의 DB 잠금을 계속 잡고 있는 구조는 피합니다.
  2. PG 호출 후: 확인한 거래 사실을 바탕으로 짧은 로컬 트랜잭션을 엽니다. 결제 결과 반영, 허용되는 주문 상태 변경, 제공 작업 등록, 발행할 이벤트 기록을 함께 커밋합니다.
  3. 커밋 후: 별도 작업자가 이벤트를 전달합니다. 수신자는 자기 처리 기록과 업무 변경을 묶어 반영하고 이미 처리한 작업이면 기존 결과를 반환합니다.

여기서 outbox는 업무 변경과 함께 저장하는 ‘전달할 이벤트’ 기록입니다. 결제 반영만 커밋되고 작업 등록은 빠지는 이중 쓰기 문제를 줄입니다. inbox는 수신한 메시지의 접수·처리 결과를 관리하는 기록입니다. 접수 완료와 업무 처리 완료는 구분해야 합니다. AWS의 transactional outbox 설명과 Particular의 NServiceBus 구현 문서도 로컬 변경·메시지 기록을 묶고 중복 처리를 별도로 다루는 구조를 설명합니다.[9][10]

이벤트를 보낸 직후 작업자가 멈추면, 보낸 사실을 기록하지 못해 같은 이벤트가 다시 전달될 수 있습니다. 따라서 outbox를 붙였다는 이유로 ‘정확히 한 번 전달된다’고 설명하지 않습니다. 중복 전달이 생겨도 같은 업무 효과가 반복되지 않도록 수신 측을 설계합니다.[9][10]

동시 콜백과 오래된 조회 결과도 같은 규칙으로 처리한다

브라우저 후속 요청, 웹훅 처리기, 주기적 복구 작업, 운영자 재처리가 각각 주문 상태를 직접 수정하면 서로의 결과를 덮을 수 있습니다. 이 경로들이 하나의 상태 전이 규칙과 중복 방지 장치를 통과하게 하는 것이 이 글의 제안입니다.

예를 들어 조회를 시작할 때 읽은 내부 상태 버전과 반영 시점의 버전을 비교합니다. 그사이에 취소 처리 등이 들어왔다면 오래된 관측값을 그대로 적용하지 않고 다시 판단합니다. 이벤트 도착 시각 하나로 최신 거래를 결정하거나, PG의 거래 식별자를 정렬 가능한 버전 번호라고 가정하지 않습니다.

내부 경합을 막아도 외부 PG와 발급처의 상태를 동시에 잠글 수 있는 것은 아닙니다. 마지막 결제 확인 직후 취소나 입금 정정이 발생하는 구간은 남습니다. 되돌릴 수 없는 제공 직전의 확인, 자체 취소 요청과 제공 작업의 조정, 제공 이후 정정에 대한 중단·회수·보상 절차를 함께 정해야 합니다. 이 경우 보상은 정책에 따른 후속 처리이지, 원래 결제를 과거로 되돌리는 DB 롤백이 아닙니다.

외부 발급의 응답 유실은 ‘다시 실행’으로 해결하지 않는다

자체 DB에 이용권 행을 만드는 일이라면 중복 방지 기록과 실제 권한 부여를 같은 트랜잭션에 넣을 수 있습니다. 같은 DB의 재고 차감도 설계한 트랜잭션 경계 안에서 다룰 수 있습니다. 그러나 외부 API가 이용권을 발급하거나 배송을 접수하는 경우에는 새로운 실패 경계가 생깁니다.

설계 예시: 발급처는 이용권을 만들었는데 응답을 보내는 중 연결이 끊겼습니다. 우리 작업자는 시간 초과만 봤습니다. 이때 내부에 ‘처리 중’ 레코드가 있다는 사실은 외부 발급의 성공도 실패도 증명하지 않습니다. 작업자 임대 시간이나 잠금 유효기간이 지났다는 이유만으로 새 발급을 허용해서도 안 됩니다.

발급처가 지원한다면 같은 작업 식별자로 결과를 조회하거나, 같은 멱등 키로 동일 요청을 반복합니다. 그런 기능이 없고 실제 발급 여부를 확인할 다른 증거도 없다면 제공 결과 미확정으로 두고 사람이 확인해야 합니다. 자동 재발급을 멈추는 것이 맞는 구간입니다. Particular의 문서 역시 outbox 트랜잭션에 참여하지 않는 외부 부작용까지 해당 보장이 확장되지는 않는다고 설명합니다.[10]

따라서 재처리 버튼은 ‘주문 전체 다시 실행’ 하나보다 ‘결제 재조회’, ‘내부 반영 복구’, ‘제공 결과 조회’, ‘확인된 미제공 작업 재실행’으로 나누는 편이 안전합니다.

웹훅은 검증해서 접수하고 중복을 견디며 처리한다

웹훅에서 중요한 것은 누가 보냈는가, 어떤 거래를 가리키는가, 그 결과를 지금 적용해도 되는가다. 인증을 통과한 과거 알림이라도 현재 상태를 덮어써도 된다는 뜻은 아닙니다.

토스의 DEPOSIT_CALLBACK은 승인 응답의 secret과 대조하는 방식을 문서화합니다. 코어 API는 결제 상태 웹훅의 Payment 객체에 대해서도 secret 대조를 설명하며 해당 필드를 nullable로 정의합니다. 따라서 신뢰할 수 있는 경로로 확보한 값이 있는 secret을 비교해야 합니다. null == null을 인증 성공으로 처리하지 않습니다. 이 값과 API 인증용 시크릿 키도 다른 개념입니다.[2][4]

또한 tosspayments-webhook-signature는 해당 문서에서 payout.changed와 seller.changed에 한정해 설명합니다. 이를 일반 결제 상태 웹훅에 그대로 요구하는 구현은 맞지 않습니다.[4]

지원되는 인증 방식이나 대조 값을 확보하지 못한 경우의 안전한 설계 대안은 알림을 상태 변경의 근거가 아니라 재조회 신호로만 사용하는 것입니다. 알려진 주문·상점·환경에 한정해 서버가 PG를 조회하고 확인된 결과로만 업무를 처리합니다. 웹훅 본문의 금액이나 상태만으로 상품을 제공하지 않습니다. 이러한 접수 경로에는 요청 크기 제한, 유효한 거래 식별자 확인, 조회량 제한도 필요합니다.

토스의 웹훅 가이드는 수신 후 10초 이내 HTTP 200 응답을 요구하며 실패 시 최대 7회 재전송한다고 안내합니다. 그래서 느린 상품 제공까지 웹훅 요청 안에서 끝내기보다, 검증 가능한 범위의 검사를 수행하고 후속 처리가 가능한 접수 기록을 영속 저장한 뒤 응답하는 구조가 적합합니다. 저장 실패까지 성공으로 응답해서는 안 됩니다. 인증이 잘못된 요청은 정상 접수와 구분합니다.[5]

비교하면 Stripe는 운영 환경의 자동 재전송, 이벤트 순서 비보장, 중복 이벤트 처리를 별도로 문서화합니다. 서명 검증에는 원본 요청 본문과 Stripe-Signature, 해당 엔드포인트의 비밀값을 사용하는 공식 방식을 적용합니다. 이 조건을 토스나 다른 PG의 보장으로 옮겨 쓰지 않습니다.[6]

수신 이벤트 ID는 추적과 중복 수신 판별에 사용하되, 그 ID의 범위와 재전송 시 유지 여부는 공급자 계약을 확인합니다. 토스의 전송 식별자가 모든 재시도에서 동일한 업무 이벤트 ID로 유지된다고 이 글에서는 가정하지 않습니다. 전달 중복을 일부 놓치더라도 마지막 상품 제공 제약이 중복 효과를 막아야 합니다.

접수 기록에는 거래 연결에 필요한 식별자, 접수 시각, 검증 결과, 처리 상태를 남기되, secret·카드정보·고객 원문을 일반 로그에 그대로 복제하지 않는 방식으로 설계합니다. PG에서 관측한 결과와 내부에서 반영한 시각도 구분하면 지연과 재처리의 경로를 확인하기 쉽습니다.

서비스 작동 방식

거래 이상에서 대응과 결제사 협의까지 — 증상별 확인 순서로 거래 상태를 확인하고 장애와 환불, 반복 문제를 같은 운영 절차로 처리합니다.

끊긴 지점에 따라 재처리할 일을 바꾼다

아래는 모두 설계 예시다. 제품의 자동 복구 기능이나 특정 PG가 보장하는 동작을 나열한 표가 아니라, 앞의 상태 분리·멱등성·로컬 트랜잭션을 실제 실패 지점에 적용한 판단표입니다.

실패 지점탐지할 차이안전한 다음 행동중복 방지 또는 중단 조건
PG 승인 후 주문 DB 커밋 실패승인 기록은 있으나 주문 반영·제공 작업 없음같은 거래를 조회하고 내부 반영 트랜잭션 복구새 승인 요청을 만들지 않음
승인 요청의 응답 유실호출 기록은 있으나 결과 미확정PG 조회 또는 기존 멱등 작업의 허용된 재시도새 주문·새 키로 결제하지 않음
콜백·웹훅이 동시에 도착동일 주문·거래에 여러 반영 시도공통 상태 전이 경로에서 직렬화·충돌 후 재조회결제 유일성 + 상품 단위 제공 제약
취소 후 오래된 승인 알림 도착관측 순서와 내부 변경 이력 불일치현재 거래 확인 후 허용되는 전이만 적용과거 성공 응답으로 제공 재개 금지
outbox 발행 직후 작업자 중단메시지 전달 여부 미확정동일 이벤트를 재전달할 수 있게 설계수신자의 처리 기록과 업무 제약
외부 상품 발급 응답 유실발급 요청은 있으나 결과 증거 없음같은 작업의 발급 결과 조회확인 불가능하면 자동 재발급 중단
주문 만료 뒤 늦은 입금 확인결제 사실과 주문 이행 가능성 불일치입금 사실 보존, 재고·유효기간·취소 정책 검토결제 기록 삭제나 무조건 제공 금지
가상계좌 입금 정정이전 결제 확인과 새 PG 결과 불일치현재 상태 반영, 진행 중 제공 중단·이미 제공한 건 검토과거 DONE만으로 계속 제공하지 않음
한 주문의 서로 다른 시도 모두 승인복수 거래와 단일 구매 의무거래 모두 기록, 추가 결제와 제공 범위를 검토결제 시도별로 같은 상품 재발급 금지

미확정 건을 찾는 복구 작업은 양방향이어야 한다

내부 주문에서 출발하는 작업만 만들면, 주문 연결 자체를 잃은 결제는 발견하지 못할 수 있습니다. 다음 두 방향을 함께 두는 것이 이 글의 제안입니다.

내부에서 PG로 확인합니다. 승인 결과 미확정, 결제 확인 후 내부 미반영, 제공 대기 장기화, 외부 제공 결과 미확정 등을 찾습니다. 각 건의 마지막 확인 시각과 담당 작업을 기록하고 이미 진행 중인 처리와 충돌하지 않게 합니다.

PG에서 내부로도 확인합니다. 토스 코어 API의 거래 조회처럼 승인·취소 이력을 얻는 공식 수단으로 내부에 대응 기록이 없는 거래를 찾습니다. 이 글에서 말하는 대사는 결제·주문·제공 연결 확인이며 수수료나 은행 입금액을 맞추는 정산 대사가 아닙니다.[2]

구체적인 수집 설계에서는 조회 구간·페이지 진행 위치를 저장하고 재수집 구간이 겹쳐도 중복 반영되지 않게 합니다. 식별자로 연결할 수 없는 거래를 금액이 같다는 이유만으로 주문에 붙이지 않습니다. 조회 한 번이 실패했거나 결과가 없었다는 이유로 ‘돈을 받지 않았다’고 단정하지도 않습니다. 조회 대상, 상점 계정, 환경, 응답 오류부터 확인해야 합니다.

HTTP 코드만으로 재시도를 분류하지 않는다

토스 오류 문서에서 ALREADY_PROCESSED_PAYMENT와 일시적 문제를 안내하는 PROVIDER_ERROR는 모두 HTTP 400 범주에 나타납니다. 따라서 ‘4xx는 모두 영구 실패’라는 공통 규칙보다 작업 종류와 오류 코드별 처리가 필요합니다.[11]

설계상 이미 처리된 결제는 실제 거래를 조회해 확인하고 입력·권한 문제는 원인을 고친 뒤 판단합니다. 일시적 오류는 기존 작업의 정체성을 유지하면서 제한된 재시도를 적용합니다. 토스의 IDEMPOTENT_REQUEST_PROCESSING은 이전 멱등 요청이 처리 중인 경우이므로, 즉시 새 결제를 만들 근거가 아닙니다.[3]

재시도는 횟수와 총 소요시간을 제한하고 간격 증가와 무작위 지연을 적용하는 방식으로 설계할 수 있습니다. 구체적인 한도는 결제 수단, 제공 기한, PG 정책과 운영 대응 능력에 맞춰 정합니다. 예시 숫자를 모든 서비스의 정답으로 삼지 않습니다.

금액·통화·주문 연결 불일치, 인증 실패, 미확정 상태의 멱등 범위 변경, 외부 제공 여부 확인 불가, 반복 실패는 수동 검토로 넘길 조건입니다. 담당자, 다음 확인 시점, 지금 금지할 행동을 같이 남깁니다. 운영자가 상태를 바꿀 때도 사유·근거·행위자·변경 전후 상태·사용한 작업 ID를 기록하고 같은 중복 방지 경로를 통과시킵니다.

정상 결제보다 실패 경계에서 검증한다

앞의 설계가 맞는지는 성공 화면 한 번으로 확인할 수 없습니다. 다음은 소유하거나 허가받은 테스트 환경에서 검증할 테스트 설계 제안입니다.

승인 직후 내부 커밋을 실패시켰을 때 추가 결제 없이 주문만 복구되는지 확인합니다. 콜백과 웹훅을 동시에 처리하고 같은 이벤트를 반복 전달해도 상품 단위의 제공 효과가 늘지 않는지 봅니다. 내부 트랜잭션 실패 시 inbox만 처리 완료로 남거나 outbox만 사라지지 않는지도 확인합니다.

취소 후 과거 승인 알림, 주문 만료 뒤 입금, 가상계좌 입금 정정, 외부 발급 성공 후 응답 유실을 각각 넣어 봅니다. 특히 외부 발급 결과를 확인할 방법이 없는 조건에서 작업이 자동 재발급 대신 미확정·검토 대기로 멈추는지가 중요합니다.

보관 기간도 검증 대상입니다. 이벤트 중복 기록을 지운 뒤 오래된 알림을 재수신해도 중복 제공이 생기지 않아야 합니다. 승인 응답이 유실된 상태에서 API 인증 키를 교체하는 경우도 별도 시나리오로 둡니다. Particular의 outbox 문서 역시 중복 제거 기록의 보관 기간을 지연·재시도 가능성과 함께 고려하도록 설명합니다.[10]

운영 화면에서는 결제 확인 후 미제공 건수, 미확정 건의 가장 오래된 경과시간, 연결되지 않은 PG 거래, 반복 실패 작업과 담당자를 함께 볼 수 있게 하는 것을 권합니다. 목적은 실패 횟수 자체보다 고객에게 남은 미해결 상태를 줄이는 것입니다.

작은 시스템이라면 작게 시작해도 된다

이 구조를 위해 반드시 메시지 브로커나 마이크로서비스부터 도입할 필요는 없습니다. 하나의 DB와 작업자가 있는 서비스라면, 영속적인 결제 시도·처리 기록, 업무에 맞는 유일성 제약, 트랜잭션으로 등록하는 후속 작업, 주기적 상태 확인부터 시작하는 설계가 가능합니다. 중요한 것은 구성요소 수보다 실패 후 이어갈 정보와 책임이 남는가입니다.

반대로 여러 PG·주문 시스템·발급처가 연결되거나, 조회할 수 없는 외부 부작용과 수동 상태 변경이 많다면 부분적인 코드 수정으로 끝내기 어렵습니다. 어느 시스템이 어떤 사실의 기준인지, 누가 예외를 판단하는지부터 함께 정리해야 합니다.

재처리는 처음부터 다시 하는 일이 아닙니다. 확인된 결제 사실을 보존하고 아직 수행하지 않았다고 확인된 다음 작업만 이어가는 일입니다. 주문 하나를 골라 세 상태와 연결 키, 마지막 실행 결과를 설명할 수 있는지부터 점검해 보자.

현재 결제·주문 흐름과 실패 시나리오를 외부에서 함께 검토해야 한다면, IXC의 PG 연동·결제 시스템 구축 서비스에서 협의할 지원 범위를 확인할 수 있습니다.