Skip to Content
← 블로그
개선기

최종 결제 확정 API로 POS 상태 불일치 해소하기

손님 폰에서는 승인, POS에서는 실패

만화로 보는 요약 — 먼저 읽어보세요
만화로 보는 요약 — 먼저 읽어보세요

QR 결제 중계 플랫폼에서 월평균 10건씩 같은 유형의 CS가 발생했습니다.

손님이 사용하는 간편결제 앱에는 결제 완료가 표시됐는데 POS는 실패로 판단했습니다.

결제 서버와 간편결제사에는 승인 거래가 남고, POS만 실패를 알고 있었습니다.

1초마다 조회해도 두 시스템은 같은 결론에 도달하지 못했다

POS는 결제를 시작한 뒤 거래 상태 조회 API인 PL을 1초마다 호출했습니다. 서버는 요청마다 RDB를 조회해 거래 상태가 성공이고 승인번호가 존재하면 성공을 반환했습니다.

간편결제사의 승인은 별도 경로로 서버에 도착합니다. 따라서 POS의 Polling과 간편결제사의 승인 통지는 서로 독립적으로 움직입니다.

POS별 Polling 만료시간은 최소 66초에서 최대 120초까지 달랐습니다. 이 구조에서 상태 불일치는 세 경로로 발생했습니다.

  • POS 네트워크 오류로 Polling이 중단됨
  • 간편결제사가 POS 만료시간 이후에 승인 결과를 통지함
  • 승인 통지와 Polling 종료가 만료시간 경계에서 엇갈림

여기서 기술적으로 중요한 점은 PL조회 API라는 사실입니다. 서버 원장에 기록된 상태는 알려줄 수 있지만, POS가 그 응답을 실제로 받았고 성공으로 처리했는지는 서버가 알 수 없습니다. Polling 횟수를 늘려도 서버 상태를 더 자주 읽을 뿐, POS와 서버 사이에 최종 합의 지점이 생기지는 않습니다.

서버에는 승인 결과가 있지만 POS가 성공 응답을 확인하지 못하면, 어느 쪽의 상태가 최종인지 판별할 별도 신호가 없었습니다. 결국 불일치는 CS가 접수된 뒤에야 알 수 있었습니다.

Push보다 API 호출 한 번을 택한 이유

해결책은 실시간성만으로 고르지 않았습니다. 레거시 POS의 기술 제약, 연동사 변경 범위, 연결 운영 비용을 함께 비교했습니다.

방식장점비용과 한계판단
SSE서버가 상태 변화를 즉시 전달레거시 POS의 SSE 구현 부담, 장시간 연결과 다수 동시 연결 관리, 연동사 변경 범위가 큼제외
Webhook서버가 결과를 능동 통지POS 단말이 HTTP 요청을 직접 수신하고 인증·재시도를 구현해야 함제외
Final API기존 PL 흐름을 유지하며 확정 신호 추가POS에 API 호출 한 번 추가 필요채택

Final API는 PL 성공 확인 → Final API 호출 → 영수증 출력 순서로 호출하게 설계했습니다. POS는 영수증을 출력하기 직전에 POS ID와 거래번호를 보내 “이 거래의 성공을 확인했고 최종 확정한다”는 의사를 서버에 남깁니다.

SSE와 Webhook은 결과를 더 빠르게 전달할 수 있지만, POS가 결과를 실제로 처리했는지 확인하려면 다시 ACK와 재시도 정책이 필요합니다. 반면 Final API는 기존 요청 방향인 POS → 서버를 유지하면서, 이미 성공을 확인한 POS가 직접 ACK를 보내도록 합니다. 전달 경로 전체를 바꾸지 않고 합의 신호만 추가한 선택입니다.

이 방식은 두 가지 문제를 함께 풀었습니다.

  1. POS와 서버가 거래 성공을 확인했다는 명시적인 합의 지점을 만들었습니다.
  2. 분쟁이 발생했을 때 Final API 호출 여부로 시스템 간 책임 경계를 확인할 수 있게 했습니다.

기존 PL 성공 처리 뒤 요청 한 번만 추가하면 되므로, 연결 방식을 바꾸는 SSE·Webhook보다 연동사 부담이 작았습니다. 신규 연동 표준으로 채택했고 기존 연동사를 포함해 약 20개 연동사 중 8개에 적용했습니다.

Final API는 승인을 만드는 API가 아니라 ACK였다

Final API는 결제를 새로 승인하지 않습니다. 이미 승인된 거래를 POS도 성공으로 확인했다는 사실을 기록하는 응용 계층의 ACK입니다. 따라서 API 계약도 승인 처리와 분리했습니다.

계약 항목처리 기준
요청 식별자POS ID, 거래번호
서버 확인승인 상태, 승인번호 존재 여부
상태 전이POS 미확정 → 확정
응답OKAY
중복 요청상태를 바꾸지 않고 동일한 OKAY

Final API는 거래 상태를 다시 확인하고 확정 정보를 기록한 뒤 응답합니다. 내부 스키마는 공개하지 않고, 글에서는 의도를 다음과 같은 설명용 필드로 표현할 수 있습니다.

설명용 필드의미
pos_finalized_ynPOS의 거래 확정 여부
final_requested_atFinal API 요청 수신 시각
final_responded_at응답 완료 시각

요청·응답 시각은 밀리초 단위로 남겼습니다. 거래번호 DB Unique 제약과 현재 상태 검사로 중복 반영을 막았고, 이미 확정된 거래에 같은 요청이 다시 오면 상태를 변경하지 않고 동일한 OKAY를 반환했습니다. 별도 멱등키를 추가하지 않고 거래번호를 멱등성 경계로 사용한 것입니다.

ACK가 없다는 이유만으로 거래를 취소하지 않았다

Final API 미호출을 곧바로 취소 근거로 사용하지 않았습니다. 배치가 대상을 ABORT로 선정한 뒤 간편결제사 거래 조회로 실제 승인 상태를 다시 확인했습니다.

ACK 미수신은 “POS와 상태가 다를 가능성”을 보여주는 약한 신호일 뿐입니다. POS가 요청을 보내지 못했을 수도 있고, 애초에 결제사 승인이 완료되지 않았을 수도 있습니다. 그래서 Final API 미호출은 조회 대상을 고르는 조건으로만 사용하고, 실제 취소 여부는 간편결제사의 승인 상태를 확인한 뒤 결정했습니다.

  • ABEND: 외부 승인이 없어 취소하지 않고 종료
  • ABAPR: 승인 거래의 망취소 성공
  • ABFAL: 망취소 실패, 1분 주기 배치에서 최대 3회 재시도

3회 모두 실패한 거래는 다음 날 운영팀 집계에 포함해 Telegram으로 알렸습니다. 일시적인 결제사 오류는 자동 재시도로 흡수하고, 반복 실패만 사람이 확인하게 했습니다.

66초는 틀린 숫자가 아니라, 적용 범위가 틀린 숫자였다

초기 배치는 다음 조건으로 취소 후보를 찾았습니다.

승인 상태
AND 승인번호 존재
AND POS 미확정
AND 거래 시작 후 66초 경과

66초는 임의의 숫자가 아니었습니다. 약 80%의 POS가 66초를 만료시간으로 사용했습니다. 문제는 나머지 20%의 POS는 최대 120초까지 정상적으로 Polling한다는 사실이었습니다. 서로 다른 POS 정책을 하나의 전역 시간값으로 축약하면서, 다수 단말에 맞는 값이 전체 단말의 규칙처럼 동작했습니다.

120초 POS에서는 거래 시작 후 66초가 지나도 아직 정상 처리 구간입니다. 하지만 배치는 다수 POS의 만료시간을 모든 POS의 공통 규칙으로 간주했고, Final API가 오지 않았다는 이유로 정상 승인 거래를 취소했습니다.

실제 운영에서 정상 승인 거래 1건이 자동 취소됐고 약 10만 원의 가맹점 매출이 사라졌습니다. 회사가 가맹점에 손실을 보상했습니다. 원인은 경계값과 POS별 만료시간 차이를 검증하지 않은 것이었습니다.

시간을 늘린 것이 아니라, 기준 시각을 바꿨다

장애 이후 배치 주기를 2분에서 1분으로 줄이고, 취소 후보 기준을 거래 시작 + 66초에서 승인 완료 + 120초로 변경했습니다.

거래 시작 시각과 승인 완료 시각은 의미가 다릅니다. 거래 시작은 비동기 결제가 출발했다는 뜻이고, 승인 완료는 결제사와 서버 원장에 승인 결과가 생겼다는 뜻입니다.

승인 전 대기시간은 결제사 응답 속도에 따라 달라집니다. 거래 시작 시각을 기준으로 삼으면 아직 Polling 중인 거래를 침범할 수 있습니다. 승인 완료 시각부터 다시 120초를 기다리면 “승인은 됐지만 POS 확정이 오지 않았다”는 더 강한 근거가 생긴 뒤에만 보상을 시작합니다.

대신 오탐을 줄인 만큼 비정상 거래를 취소하기까지 기다리는 시간은 길어졌습니다. 1분 주기 배치이므로 기준 시각을 지난 뒤 실제 탐지까지 최대 한 주기가 더 필요합니다. 정확성과 고객·가맹점 대기시간 사이의 트레이드오프를 감수한 결정이었습니다.

배치 시간 경계와 POS별 만료시간 차이 같은 예외 조건을 중심으로 테스트도 보강했습니다.

월 10건을 1건으로 줄인 뒤, 운영의 기준도 달라졌다

항목개선 전개선 후
상태 불일치 CS월평균 10건도입 후 월평균 1건, 90% 감소
비정상 거래 처리CS 이후 수동 확인월평균 6~7건 자동 감지·취소
최종 상태 판별서버 승인 상태만 존재POS의 명시적 확정 신호 추가
적용 범위Final API 없음약 20개 연동사 중 8개, 신규 연동 표준

문제 분석, 대안 비교, 사내 PT, Final API 설계·구현, 망취소 배치와 Telegram 알림, 테스트·배포·운영을 직접 담당했습니다. POS 연동사와의 외부 협의는 담당 범위에서 제외했습니다.

상태 불일치 CS가 줄면서 운영팀과 개발팀이 거래 원장을 반복해서 대조하고 후속 조치하던 공수도 함께 줄었습니다.

다시 설계한다면 Deadline과 상태 전이를 원장에 남긴다

현재 개선은 관측된 오탐을 막았지만, 시간만 늦춘다고 동시성 문제가 사라지는 것은 아닙니다. 다시 설계한다면 세 시간을 분리하겠습니다.

  • POS별 Polling 만료시간
  • 승인 후 Final API를 기다리는 확정 유예시간
  • 배치 실행 주기

승인 시점에 finalization_deadline_at 같은 확정 기한을 계산해 거래에 저장하고, 배치는 POS 미확정 AND 확정 기한 경과만 조회하게 하겠습니다. 정책이 바뀌어도 과거 거래의 기준이 흔들리지 않고, 66초 같은 값이 여러 코드에 흩어지는 것도 막을 수 있습니다.

또한 Final API와 배치가 동시에 같은 거래를 처리하지 못하도록 조건부 상태 전이를 사용하겠습니다. Final API는 아직 ABORT로 선점되지 않은 거래만 확정하고, 배치는 POS 미확정 상태인 거래만 원자적으로 ABORT로 전환한 뒤 외부 조회를 시작합니다.

마지막으로 ABFAL 3회 실패는 다음 날 집계만 기다리지 않고 즉시 운영 알림을 보내고, 다음 날 집계는 누락 방지용 요약으로 분리하겠습니다. 자동화의 목표는 사람을 완전히 빼는 것이 아니라, 사람이 개입해야 할 실패를 더 빠르고 명확하게 선별하는 것이기 때문입니다.