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

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

결제 응답 — SUCCESS / FAILED / UNKNOWN

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

Fail-Fast를 통한 가용성 보호 — 서킷 브레이커

notion image
외부 시스템인 PG는 제어할 수 없는 영역이며, 장애 발생 시 무의미한 재시도가 반복되면 지연이 누적되고 시스템 리소스가 소모되며 장애가 내부로 전파될 수 있다.
이를 방지하기 위해, PG 호출 전 단계에서 서킷 브레이커를 적용하여 장애를 조기 감지하고, Fail-Fast로 차단하도록 설계하였다.
 

Sliding Window 방식 선택 — Time-based

서킷 브레이커의 실패율 판단 기준으로 Time-based Sliding Window를 선택하였다.
  • 트래픽 변동이 있는 환경에서 현재 상태를 빠르게 반영 가능
  • 과거 장애 데이터가 자동 소멸되며, 현재 상태를 더 정확히 반영
우리 서비스는 시간대에 따라 요청량 변동이 크기에, 현재 시점의 PG 상태를 빠르게 반영하는 것이 맞다고 판단하였다.
 

PG 타임아웃 전략 — 11초의 기준보다 길게

notion image
카카오페이측의 리드 타임아웃은 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초 이상 지연만 감지하도록 하여 오탐 최소화
      • notion image
  • waitDurationInOpenState 30초로 설정
    • 장애 발생 직후 즉시 재시도하지 않고, PG 시스템 회복 시간 충분히 확보한 뒤 복구 여부 재판단
  • permittedNumberOfCallsInHalfOpenState 3건으로 설정
    • 복구 구간에서 소수의 요청만 우선 허용해, 정상 복귀 여부를 점진적으로 검증
 

가설 : P99 수준의 지연을 Fail-Fast로 차단하여, 시스템 전체 지연 확산을 방지할 수 있는가?

  • 측정 대상 : 네트워크 RTT + PG 내부 처리 시간 (KakaoPay Test API 이용)
  • 검증 목표 :
    • P99 이상의 지연이 시스템 내부로 전파되지 않고 차단되는지 검증
    • 일시적 오류(약 30%)는 허용하고, 구조적 장애(50% 이상)는 즉시 차단되는지 확인
    • 장애 전파 차단 시간 측정 : PG 지연 시작 시점 ~ 서킷 OPEN으로 전환되어 커넥션 점유가 해제되기까지의 시간
    • Half-Open 상태에서의 복구 안정성 검증 : permittedNumberOfCalls 내 성공 비율이 CLOSED 전환에 충분한지 확인

테스트 결과

검증 항목 1 — P99 지연 전파 차단 검증
notion image
PG사 지연이 최대 8초까지 걸리는 상황에서 100ms대 응답이 나올 수 있는 이유는, 서킷 브레이커 OPEN 이후 98건의 요청이 PG로 가지 않고 내부에서 즉시 Fallback 처리되었기 때문이다.
notion image
notion image
지연 구간 별 RPS 유지
지연 구간 별 RPS 유지
SocketTimeoutException 등 예외 상황에도 의도한 Fallback 응답을 반환하여 HTTP 레벨에서 실패율 0%를 유지하고, 약 11.85% 수준에서 차단 후 Fallback 응답으로 전환되었다.
 
검증 항목 2 — 일시적 오류(30%) 허용 및 장애(50%↑) 차단
notion image
notion image
  • 4초 지연 및 30% 에러 구간에서는 Fallback Rate가 0%로 유지되며 정상 처리
  • 반면 Phase 6(70% 에러) 진입 시 Fallback Rate가 급격히 상승하며 서킷이 OPEN
slowCallDurationThreshold(6초) 및 failureRateThreshold(50%)이 정상 지연(3~5초)과 구조적 장애를 명확히 구분하여 동작함을 확인
 
검증 항목 3 — 장애 전파 차단 속도
notion image
  • 장애 상황이 발생한 지 169ms에 서킷이 임계치 초과임을 감지하고 OPEN으로 전환 ⇒ 실시간에 가까운 커넥션 고갈 및 리소스 차단 증명
 
검증 항목 4 — Half-Open 상태 복구 안정성 검증
notion image
  • Phase 3 (8초 지연 구간)가 60초에 시작되자마자 서킷이 열렸고(약 60.7초 추정)
  • 그 후 정확히 30초가 지난 90.7초에 서킷이 Half-Open으로 전환되며 첫 복구 성공을 기록 (waitDurationInOpenState 30초)
notion image
notion image
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) • 비즈니스 검증 실패 (잔액 부족 등) • 필수 데이터 누락
잘못된 요청이거나 로직 위반으로, 재시도해도 결과가 변하지 않음
즉시 종료 및 사용자에게 에러 안내
notion image
초기 결제 승인 응답(PG)으로 500에러 또는 타임아웃이 반환되는 경우, 실패로 확정짓지 않고 결제 단건 조회를 수행한다.
PG 조회 시 아래와 같은 결제 상태값을 내려주기에, 이를 통해 최종 상태를 확정한다.
  • READY_PATMENT: 보상 작업 예약
  • SUCCESS_PAYMENT: 결제 완료 ⇒ 성공 반환
  • CANCEL_PAYMENT: 결제 취소 ⇒ 취소 반환
  • FAIL_PAYMENT: 결제 실패 ⇒ 실패 반환
아래 3개의 경우 즉시 각 응답값을 사용자에게 반환하지만,
READY_PAYMENT는 사용자가 결제를 포기한 경우/승인 요청은 갔지만 타임아웃/네크워크 장애 등등으로 유실된 경우에 해당한다.
 

결제 상태 불일치 해소 — Outbox & 보상 트랜잭션

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

결제 후 처리 흐름 통합 — Facade 기반 오케스트레이션

notion image
결제 이후의 지급, 취소, Outbox 처리 흐름은 생각보다 많은 분기와 책임을 포함하고 있었다.
이를 각 서비스에 분산시키지 않고 PaymentItemGrantFacade로 통합함으로써, 복잡한 후속 처리 로직을 하나의 유스케이스로 캡슐화했다.
그 결과 호출부는 단순해지고 변경 영향 범위는 줄어들었으며, 트랜잭션 경계와 보상 처리 흐름 또한 한 곳에서 일관되게 제어할 수 있게 되었다.
 

결론 및 정리

모든 복구 작업은 현재 상태를 먼저 조회하고 진행되므로, 동일한 작업이 여러 번 수행되어도 멱등성이 보장된다.
해당 설계의 최고 장점은 개발자가 개입하지 않아도, 시스템이 스스로 오류를 인지하고 정합성을 맞춘다는 것이다.