결제 응답 불확실성 — 타임아웃, PG 내부 지연

결제 과정에서 발생하는 타임아웃은 크게 두가지이다.
- Connection Timeout : PG 서버와의 TCP 연결 자체가 실패한 경우
- Read Timeout : 요청은 전달되었으나, 최종 응답을 받기 전에 타임아웃이 발생한 경우
이 중 가장 위험한 상황은, PG에서는 결제가 완료되었지만 우리 서버는 이를 인지하지 못하는 경우다.
이때 시스템은 실제 결제와 다른 상태를 기록하게 되어 정합성 깨짐과 동시에 사용자 경험 저하로 이어질 수 있다.
결제 응답 — SUCCESS / FAILED / UNKNOWN

UNKNOWN(알 수 없음)은 네트워크 지연·장애·타임아웃 등으로 인해
백엔드 서버가 PG 응답의 최종 상태를 알 수 없는 경우를 말한다.
대표적인 사례는 다음과 같다.
- Read Timeout으로 응답 수신 실패
- 요청/응답 과정에서의 패킷 유실
- PG 내부 처리 지연 또는 장애로 인한 미확정 응답
이 경우 이미 사용자의 돈이 빠져나간 상태일 수 있으니,
상태 확정을 해서는 안되며 재시도, 상태 조회, 취소 중 어떤 전략을 선택할지 결정해야 한다.
Fail-Fast를 통한 가용성 보호 — 서킷 브레이커

외부 시스템인 PG는 제어할 수 없는 영역이며, 장애 발생 시 무의미한 재시도가 반복되면
지연이 누적되고 시스템 리소스가 소모되며 장애가 내부로 전파될 수 있다.
이를 방지하기 위해, PG 호출 전 단계에서 서킷 브레이커를 적용하여 장애를 조기 감지하고, Fail-Fast로 차단하도록 설계하였다.
Sliding Window 방식 선택 — Time-based
서킷 브레이커의 실패율 판단 기준으로 Time-based Sliding Window를 선택하였다.
- 트래픽 변동이 있는 환경에서 현재 상태를 빠르게 반영 가능
- 과거 장애 데이터가 자동 소멸되며, 현재 상태를 더 정확히 반영
우리 서비스는 시간대에 따라 요청량 변동이 크기에,
현재 시점의 PG 상태를 빠르게 반영하는 것이 맞다고 판단하였다.
PG 타임아웃 전략 — 11초의 기준보다 길게

카카오페이측의 리드 타임아웃은 11초로 되어 있지만, 실제 응답 시간은 네트워크 지연을 포함하므로
11초 시점에 요청을 종료할 경우 정상 응답을 놓칠 수 있다.
따라서 약간의 버퍼를 더한 약 15초를 대기 기준으로 설정하였다.
- 네트워크 지연을 고려해 임계값 근처 응답 손실 방지
- PG 최대 처리 시간이 지난 이후 상태 조회를 수행함으로써 정합성 판단의 확실성 확보
- PG가 먼저 타임아웃을 반환하도록 유도하여 연결 종료 과정의 안정성 확보
Time-Based Sliding Window 검증
운영 환경에서 관측된 피크 트래픽 RPS(2~5)을 기준으로
서킷 브레이커의 동작을 검증하기 위한 테스트를 진행하였다.
[테스트 조건]
- 모니터링 시스템의 지표 수집 주기(15초)를 고려하여 slidingWindowSize를 20초로 설정
- 대시보드 지표와 서킷 브레이커 내부 판단 기준을 최대한 일치시켜 운영 가시성 확보
- 저트래픽에서도 안정적인 판단이 가능하도록 minimumNumberOfCalls를 5건으로 설정
- 1~2건 수준의 일시적 오류만으로 서킷이 OPEN되는 오탐 방지
- slowCallDurationThreshold 6초로 설정
- 정상 범주로 볼 수 있는 카드사 지연(3~5초)은 허용하고, 시스템 보호가 필요한 6초 이상 지연만 감지하도록 하여 오탐 최소화

- waitDurationInOpenState 30초로 설정
- 장애 발생 직후 즉시 재시도하지 않고, PG 시스템 회복 시간 충분히 확보한 뒤 복구 여부 재판단
- permittedNumberOfCallsInHalfOpenState 3건으로 설정
- 복구 구간에서 소수의 요청만 우선 허용해, 정상 복귀 여부를 점진적으로 검증
가설 : P99 수준의 지연을 Fail-Fast로 차단하여, 시스템 전체 지연 확산을 방지할 수 있는가?
- 측정 대상 : 네트워크 RTT + PG 내부 처리 시간 (KakaoPay Test API 이용)
- 검증 목표 :
- P99 이상의 지연이 시스템 내부로 전파되지 않고 차단되는지 검증
- 일시적 오류(약 30%)는 허용하고, 구조적 장애(50% 이상)는 즉시 차단되는지 확인
- 장애 전파 차단 시간 측정 : PG 지연 시작 시점 ~ 서킷 OPEN으로 전환되어 커넥션 점유가 해제되기까지의 시간
- Half-Open 상태에서의 복구 안정성 검증 : permittedNumberOfCalls 내 성공 비율이 CLOSED 전환에 충분한지 확인
테스트 결과
검증 항목 1 — P99 지연 전파 차단 검증

PG사 지연이 최대 8초까지 걸리는 상황에서 100ms대 응답이 나올 수 있는 이유는,
서킷 브레이커 OPEN 이후 98건의 요청이 PG로 가지 않고 내부에서 즉시 Fallback 처리되었기 때문이다.



SocketTimeoutException 등 예외 상황에도
의도한 Fallback 응답을 반환하여 HTTP 레벨에서 실패율 0%를 유지하고,
약 11.85% 수준에서 차단 후 Fallback 응답으로 전환되었다.
검증 항목 2 — 일시적 오류(30%) 허용 및 장애(50%↑) 차단


- 4초 지연 및 30% 에러 구간에서는 Fallback Rate가 0%로 유지되며 정상 처리
- 반면 Phase 6(70% 에러) 진입 시 Fallback Rate가 급격히 상승하며 서킷이 OPEN
slowCallDurationThreshold(6초) 및 failureRateThreshold(50%)이
정상 지연(3~5초)과 구조적 장애를 명확히 구분하여 동작함을 확인
검증 항목 3 — 장애 전파 차단 속도

- 장애 상황이 발생한 지 169ms에 서킷이 임계치 초과임을 감지하고 OPEN으로 전환 ⇒ 실시간에 가까운 커넥션 고갈 및 리소스 차단 증명
검증 항목 4 — Half-Open 상태 복구 안정성 검증

- Phase 3 (8초 지연 구간)가 60초에 시작되자마자 서킷이 열렸고(약 60.7초 추정)
- 그 후 정확히 30초가 지난 90.7초에 서킷이
Half-Open으로 전환되며 첫 복구 성공을 기록 (waitDurationInOpenState 30초)


190초에 Half-Open 상태로 전환된 후,
permittedCalls(3건)의 성공 여부를 확인 후 2초 뒤에 CLOSED 되었다.
서킷 브레이커 감지 대상 / 비감지 대상 예외
분류 | 대상 케이스 (Event) | 코드상 발생 지점 / 측정 방식 | 비고 및 상세 내용 |
장애 | 최종 타임아웃 | ResourceAccessException,
SocketTimeoutException | API 호출 지연 후, 보상 로직(상태 조회)에서도 성공을 확인하지 못해 예외를 다시 던진 경우 |
ㅤ | 네트워크 단절 | ResourceAccessException | Connect Timeout |
ㅤ | 응답 지연 | Resilience4j 내부 측정 | 응답은 성공(200 OK)했으나,
설정된 임계치를 초과한 호출 |
ㅤ | API 서버 에러 | BusinessException | PG 서버의 4xx/5xx 응답 |
ㅤ | 기타 예외 | RuntimeException | 모든 런타임 에러 |
정상 | 정상 응답 | - | ㅤ |
ㅤ | 보상 로직 성공 | - | 1차 호출은 지연, 상태 조회를 통해 성공을 확인한 경우 |
ㅤ | 재시도 성공 | - | 1차 시도는 실패, 2차 시도 중 성공한 경우 |
ㅤ | 차단 중 요청 | - | 이미 서킷이 OPEN된 상태에서 들어와 즉시 거절(Fail-Fast)된 요청. |
결제 조회 상태 기반 비동기 복구 플로우
서킷 브레이커에 더해 추가적인 안전 장치들을 도입하였다.
초기 결제 승인 응답 복구/실패 처리 분류 기준
복구 여부는 결과가 이후에 변경될 가능성이 있는지를 기준으로 판단하였다.
구분 | 결제 상태 | 주요 발생 원인 | 판단 근거 | 후속 조치 |
복구 시도 대상
| PENDING | • Read Timeout, 응답 유실
• PG사 내부 지연
• HTTP 5xx (Server Error)
• 일시적 네트워크 장애 | 결과가 아직 확정되지 않았으며, 재시도 시 성공으로 바뀔 가능성이 있음 | PG사 상태 조회 후
동기화 또는 취소 |
즉시 실패 처리
| FAILED | • HTTP 4xx (Client Error)
• 비즈니스 검증 실패 (잔액 부족 등)
• 필수 데이터 누락 | 잘못된 요청이거나 로직 위반으로, 재시도해도 결과가 변하지 않음 | 즉시 종료 및
사용자에게 에러 안내 |

초기 결제 승인 응답(PG)으로 500에러 또는 타임아웃이 반환되는 경우,
실패로 확정짓지 않고 결제 단건 조회를 수행한다.
PG 조회 시 아래와 같은 결제 상태값을 내려주기에, 이를 통해 최종 상태를 확정한다.
READY_PATMENT: 보상 작업 예약
SUCCESS_PAYMENT: 결제 완료 ⇒ 성공 반환
CANCEL_PAYMENT: 결제 취소 ⇒ 취소 반환
FAIL_PAYMENT: 결제 실패 ⇒ 실패 반환
아래 3개의 경우 즉시 각 응답값을 사용자에게 반환하지만,
READY_PAYMENT는 사용자가 결제를 포기한 경우/승인 요청은 갔지만 타임아웃/네크워크 장애 등등으로 유실된 경우에 해당한다.
결제 상태 불일치 해소 — Outbox & 보상 트랜잭션
보상 작업의 목적은 ‘DB의 결제 상태와 PG 실제 상태 간 불일치’를 해소하는 것이다.

초기 승인 요청에서 확정 짓지 못한 결제건은 UNKOWN으로 Outbox에 저장되며, 이후 비동기 복구 프로세스를 통해 최종 정합성을 보장한다.
- Success에서 결제 취소를 진행하는 이유는 현재 프로세스는 이미 사용자에게 Fallback 응답이 나간 이후 PG-DB간 상태 정합성을 맞추는 과정이기 때문
- 응답 없음의 경우 배치로도 PG 응답이 오지 않은 상황이며, 재시도 최종 실패 시 수동 처리의 영역으로 넘어간다.
비동기 배치의 장점은 코드 레벨에서 수행하는 방식과 비교했을 때 드러난다. 장애가 발생했을 시 스케줄러 재시도를 통해 복구가 가능하다는 것이다.
만약 취소 API를 try-catch로 잡아서 코드단에서 직접 호출한다면, 서버의 인터넷이 끊기거나 카카오 서버가 잠깐 마비될 경우 유저의 돈은 결제되었으나 롤백은 실패하고, 취소 기록조차 증발 할 수 있다.
결제 후 처리 흐름 통합 — Facade 기반 오케스트레이션

결제 이후의 지급, 취소, Outbox 처리 흐름은 생각보다 많은 분기와 책임을 포함하고 있었다.
이를 각 서비스에 분산시키지 않고
PaymentItemGrantFacade로 통합함으로써,
복잡한 후속 처리 로직을 하나의 유스케이스로 캡슐화했다.그 결과 호출부는 단순해지고 변경 영향 범위는 줄어들었으며,
트랜잭션 경계와 보상 처리 흐름 또한 한 곳에서 일관되게 제어할 수 있게 되었다.
결론 및 정리
모든 복구 작업은 현재 상태를 먼저 조회하고 진행되므로,
동일한 작업이 여러 번 수행되어도 멱등성이 보장된다.
해당 설계의 최고 장점은 개발자가 개입하지 않아도,
시스템이 스스로 오류를 인지하고 정합성을 맞춘다는 것이다.



