금융 결제 시스템에서 TimeoutException 완전 정복하기
실시간 정산과 펌뱅킹에서 마주한 타임아웃 전쟁기
여는 글
실시간 정산/지급대행 테스트 중 시스템에서 TimeoutException이 발생했는데, 정작 고객의 계좌에는 돈이 들어왔다는 것이었죠. 이 순간부터 시작된 타임아웃과의 전쟁, 그리고 금융 결제 시스템의 복잡한 계층 구조에서 발생하는 다양한 타임아웃 이슈들을 정리해보았습니다.
사건의 발단 - 불일치의 시작
실시간 정산과 지급대행 시스템을 개발하면서 다음과 같은 아키텍처를 구성했습니다.
CLIENT(가맹점/사용자) → PG(우리 시스템) → 금융 VAN(HYPEN/DAZN) → BANK
이 파이프라인은 카드결제와 유사하지만 다른점이 있습니다. 소요시간에 대한 문제인데요. 카드결제에 비해 계좌이체 라는 작업은 많은 검증을 거치는 로직입니다. 은행과 금융 VAN측 소스는 정확히 알 수 없지만, 환경에 따라 수 초가 소요되는 작업입니다. 따라서, TimeoutException 처리 방식에 대해 금융 VAN과의 협업을 통해 명확히 해야합니다.
TimeoutException의 종류 - Connection Timeout vs Read Timeout
Connection Timeout: TCP 연결 수립 실패
Connection Timeout은 TCP 3-way handshake를 완료하는 데 걸리는 시간입니다. 서버가 다운되었거나, 방화벽에서 차단되었거나, 네트워크 경로에 문제가 있을 때 발생합니다.
발생 원인
- 상대방 서버 다운
- 방화벽/보안그룹 설정 오류
- 잘못된 IP/포트 설정
- 네트워크 라우팅 문제
Connection Timeout이 발생하면 요청 자체가 전달되지 않은 것이므로, 재시도해도 중복 거래 위험이 없습니다.
Read Timeout: 응답 대기 시간 초과
Read Timeout은 연결은 성공했지만 응답이 오지 않을 때 발생합니다. 이게 금융 거래에서 가장 골치 아픈 상황입니다. 요청은 전달되었는데 결과를 모르는 상태이기 때문입니다.
펌뱅킹에서 Read Timeout이 자주 발생하는 이유
- 은행 처리 지연: 대용량 이체나 의심 거래는 은행 내부의 추가 검증을 거칩니다.
- VAN사 대기열: 월말이나 급여일에는 거래가 몰려 처리 시간이 길어집니다.
- 네트워크 지연: 암호화 통신 구간에서의 패킷 지연이나 손실.
참고: Java의
socket.setSoTimeout()은 Read Timeout과 동일한 개념입니다. 소켓에서 데이터를 읽는 동안의 타임아웃을 설정합니다.
계층별 타임아웃 설정의 함정
실제 운영 중 발견한 가장 큰 문제는 타임아웃 체인의 역전 현상이었습니다.
이상적인 타임아웃 체인:
- Client Timeout: 60초
- PG Timeout: 50초
- VAN Timeout: 40초
- Bank Timeout: 30초
하위 계층으로 갈수록 타임아웃이 짧아야 상위 계층에서 적절히 대응할 수 있습니다. 그런데 실제로는 VAN 타임아웃이 40초인데 PG에서 30초만 기다리는 설정 실수가 있었습니다. 결과적으로 VAN은 아직 은행 응답을 기다리는 중인데, 우리는 이미 타임아웃으로 실패 처리를 해버린 것입니다.
타임아웃 발생 시 재처리 전략
일반적으로 금융 VAN사들은 타임아웃 건에 대한 재처리 가이드와 이체 결과 조회 API를 제공합니다. 하지만 단순히 VAN사 가이드를 따르는 것을 넘어, PG 측에서 어떻게 데이터를 핸들링하고 비즈니스 연속성을 보장할 것인지가 핵심입니다.
1. 트랜잭션 상태 관리 아키텍처
가장 먼저 구축해야 할 것은 정교한 상태 관리 시스템입니다. 단순히 성공/실패 두 가지 상태만으로는 타임아웃 상황을 제대로 다룰 수 없습니다. PENDING, PROCESSING, TIMEOUT, SUCCESS, FAILED와 같은 세분화된 상태를 정의하고, 각 상태 전이마다 타임스탬프와 사유를 기록해야 합니다.
특히 중요한 것은 TIMEOUT 상태입니다. 이는 “아직 끝나지 않은 거래”를 의미하며, 주기적인 상태 확인과 재처리가 필요한 대상입니다.
2. 지능형 재처리 전략
타임아웃이 발생하면 즉시 VAN사의 거래 조회 API를 호출합니다. 여기서 나올 수 있는 결과는 크게 네 가지입니다.
SUCCESS (팬텀 성공): 우리는 타임아웃으로 실패 처리했지만 실제로는 성공한 경우입니다. 이 경우 즉시 내부 상태를 성공으로 변경하고 클라이언트에게 알림을 보내야 합니다. 동시에 정산 데이터의 정합성도 체크해야 합니다.
PROCESSING: 아직 VAN이나 은행에서 처리 중인 경우입니다. 이때는 지수 백오프(exponential backoff) 전략으로 재조회 간격을 늘려가며 확인합니다.
NOT_FOUND: VAN사에서도 해당 거래를 찾을 수 없는 경우입니다. 네트워크 단에서 요청이 유실되었을 가능성이 높으므로, 재시도 한계 내에서는 새로운 요청을 시도합니다.
FAILED: 명확한 실패입니다. 이 경우 실패 사유를 분석하여 재시도 가능 여부를 판단합니다.
3. 보상 트랜잭션 패턴
재시도 한계에 도달했거나 특정 VAN에서 계속 문제가 발생한다면, 보상 트랜잭션을 생성합니다. 이는 원거래와는 별개의 새로운 거래로, 다음과 같은 특징을 가집니다
- 원거래와의 연결 관계를 명확히 유지
- 대체 VAN 경로를 통한 우회 처리
- 더 보수적인 타임아웃 설정 (평소의 2배)
- 실패 시 즉시 수동 개입 알림
4. 데이터 일관성 보장 전략
금융 거래에서 가장 중요한 것은 데이터 일관성입니다. 타임아웃 상황에서도 이를 보장하기 위해 여러 가지 전략을 적용했습니다.
멱등성 키(Idempotency Key) 기반 중복 방지
모든 이체 요청에 고유한 멱등성 키를 부여합니다. 타임아웃 후 재시도 시에도 동일한 키를 사용하면, VAN사와 은행에서 중복 거래를 자동으로 걸러냅니다. 키 생성 규칙은 {가맹점ID}_{거래일자}_{순번} 형태로, 같은 거래 의도에 대해서는 항상 같은 키가 생성되도록 설계했습니다.
트랜잭션 로그 테이블 운영
거래 요청 시점에 먼저 로그 테이블에 PENDING 상태로 기록하고, 이후 VAN 응답에 따라 상태를 갱신합니다. 타임아웃이 발생하면 TIMEOUT 상태로 마킹되어 별도의 재처리 배치 대상이 됩니다. 이 로그는 원장과 별개로 관리되어, 장애 상황에서도 거래 흐름을 추적할 수 있습니다.
주기적 정합성 체크 배치
매 시간마다 PG 원장과 VAN사 거래 내역을 대사(reconciliation)합니다. 불일치 건이 발견되면 자동으로 상태를 보정하거나, 금액 차이가 있는 경우 담당자에게 알림을 발송합니다. 특히 타임아웃으로 PENDING 상태에 머물러 있는 거래는 우선적으로 VAN 조회 API를 통해 실제 결과를 확인합니다.
Outbox 패턴 (선 기록 후 실행)
실제 이체 요청을 보내기 전에 “이체를 시도할 것이다”라는 의도를 먼저 기록합니다. 요청 전에 PENDING 상태로 기록하고, 응답을 받으면 결과에 따라 상태를 갱신합니다. 이렇게 하면 시스템 장애로 인해 응답을 받지 못하더라도, 재시작 후 미완료 거래를 식별하고 후속 처리를 이어갈 수 있습니다.
외부 API 연동 시 실패 지점 파악
타임아웃이 발생했을 때 가장 먼저 해야 할 일은 어디서 문제가 발생했는지 파악하는 것입니다. “우리 시스템 문제인가, 외부 API 제공업체 문제인가?”를 구분하지 못하면 원인 분석도, 책임 소재 파악도 어렵습니다.
구간별 타임스탬프 기록
외부 API 호출 시 다음 시점을 모두 기록해야 합니다.
T1: 요청 시작 시각 (API 호출 직전)
T2: 연결 완료 시각 (TCP 연결 성공)
T3: 요청 전송 완료 시각 (Request Body 전송 완료)
T4: 응답 수신 시작 시각 (첫 번째 응답 바이트 수신)
T5: 응답 수신 완료 시각 (전체 응답 수신)
이 타임스탬프를 기록해두면 타임아웃 발생 시 어느 구간에서 지연이 발생했는지 파악할 수 있습니다.
- T2 - T1이 길다면: 네트워크 문제 또는 상대 서버 다운
- T4 - T3이 길다면: 외부 API 처리 지연 (상대방 책임 가능성 높음)
- T5 - T4가 길다면: 응답 데이터가 크거나 네트워크 대역폭 문제
로그 설계 예시
타임아웃 발생 시 다음 정보가 로그에 남아야 합니다.
[TIMEOUT] txId=TXN20250729001
| target=VAN_HYPEN
| endpoint=/api/v1/transfer
| failurePoint=PROCESSING
| connectionTime=127ms
| waitingTime=30247ms (timeout at 30000ms)
| requestSize=1.2KB
| lastStatus=PENDING
이 로그만 보면 “연결은 127ms 만에 됐는데, 응답을 30초 넘게 기다리다가 타임아웃 발생 → VAN사 처리 지연”이라는 것을 알 수 있습니다.
책임 소재 판단 기준
| 실패 지점 | 주요 원인 | 책임 소재 |
|---|---|---|
| CONNECTION | 서버 다운, 방화벽, 네트워크 | 확인 필요 (양측 점검) |
| PROCESSING | 외부 API 처리 지연 | 외부 API 제공업체 |
| NETWORK | 패킷 손실, 대역폭 부족 | 네트워크 인프라 |
물론 책임 소재가 명확하더라도 서비스 장애는 우리 책임입니다. 중요한 것은 원인을 정확히 파악하고 적절한 대응을 하는 것입니다. 외부 API 지연이 잦다면 타임아웃을 늘리거나, 대체 API를 검토하거나, 비동기 처리로 전환하는 등의 아키텍처 개선을 고려해야 합니다.
교훈과 베스트 프랙티스
1. 타임아웃은 계층적으로 설계하라
상위 계층일수록 타임아웃을 길게 설정해야 합니다. 그래야 하위 계층에서 발생한 지연을 상위에서 적절히 처리할 수 있습니다.
2. 타임아웃 ≠ 실패
특히 금융 거래에서는 타임아웃이 발생해도 실제 거래는 성공할 수 있습니다. 항상 상태 확인을 우선하고, 섣부른 실패 처리는 피해야 합니다.
3. 멱등성은 필수, 선택이 아니다
모든 금융 API는 멱등성 키를 가져야 하며, 타임아웃 후 재시도 시에도 같은 키를 사용해야 합니다. 이는 중복 거래를 방지하는 가장 기본적인 안전장치입니다.
4. 로깅은 디버깅의 시작
타임아웃 발생 시점, 소요 시간, 발생 위치를 정확히 기록해야 합니다. 특히 어느 구간(PG-VAN, VAN-Bank)에서 발생했는지 구분하는 것이 중요합니다.
5. 부하 테스트는 실제 환경처럼
테스트 시나리오에 반드시 타임아웃 케이스를 포함시켜야 합니다. 정상 응답뿐 아니라 지연 응답, 타임아웃, 네트워크 단절 등 다양한 상황을 시뮬레이션해야 실제 운영에서 당황하지 않습니다.
맺는글
TimeoutException은 분산 시스템에서 피할 수 없는 현상입니다. 특히 금융 결제 시스템처럼 여러 기관이 연계된 환경에서는 더욱 복잡해집니다.
중요한 것은 다음과 같습니다.
- 타임아웃을 실패로 단정 짓지 말 것: 특히 금융 거래에서는 치명적일 수 있습니다
- 계층별 타임아웃 설계: 상위 계층일수록 관대하게
- 상태 확인 메커니즘: 타임아웃 후 반드시 거래 상태 확인
- 멱등성 보장: 재시도가 중복 거래로 이어지지 않도록
- 모니터링과 분석: 패턴을 파악하여 선제적 대응