
본 프로젝트는 토스 페이먼츠 결제 API를 활용한 방탈출 예약 서비스입니다.
해당 글은 아래의 3가지 조건에 기반하여 결제 API 연동 과정에서 트랜잭션 관리, 원자성 보장, 데이터 무결성 확보 등을 위해 선택한 기술적 접근 방식에 대해 설명합니다.
→ 트랜잭션 분리 전략 = 🔗 결제 API - 트랜잭션 분리 전략으로 강한/최종 일관성 보장
→ 모든 작업을 동기 방식으로 구성 = 🔗 결제 API - 트랜잭션 분리 전략으로 강한/최종 일관성 보장
해당 글에서는
조건 3을 충족하는 과정을 담고 있으며,조건 1과조건 2를 충족하는 과정은
🔗 결제 API - 트랜잭션 분리 전략으로 강한/최종 일관성 보장에서 확인 가능합니다.
조건 3 : 사용자는 예약이 즉시 완료되기를 기대하므로 Time Out을 설정해야 한다.
결론적으로 Transaction timeout과 Http timeout을 설정하는것으로 해결하였습니다.

외부 결제 API와 예약/결제 사전 정보를 DB에 저장하는 작업을 하나의 트랜잭션으로 묶게 되면, 외부 API 호출에 소요되는 지연 시간 동안 내부 DB 커넥션이 계속해서 점유되므로 커넥션 고갈 문제가 발생할 수 있습니다.

이러한 상황은 내부 DB 접근에 제약을 주어 전체 시스템의 성능 저하 및 장애로 이어질 위험이 있다고 판단하였습니다.
또한 사용자는 예약이 즉시 완료되기를 기대하지만, 지연 시간 동안 사용자 경험이 저하되어 불만이 쌓이고, 이는 궁극적으로 고객 이탈로 이어질 수 있다고 판단하였습니다.
외부 결제 API 호출 전에 DB에 예약/결제 사전 정보를 저장하는 작업과 외부 결제 API 호출은 하나의 트랜잭션으로 처리하고, 그 후 예약/결제 상세 정보를 저장하는 작업은
별도의 트랜잭션으로 분리
// ReservationFacade.java
public ReservationPaymentResponse saveReservationPayment(
LoginMember loginMember,
ReservationPaymentRequest reservationPaymentRequest
) {
ReservationPaymentResult reservationPaymentResult = reservationApplicationService.saveAdvanceReservationPayment(loginMember, reservationPaymentRequest);
try {
return reservationApplicationService.saveDetailedReservationPayment(reservationPaymentResult.reservation(), reservationPaymentResult.paymentResult());
} catch (Exception e) {
return new ReservationPaymentResponse(
ReservationResponse.from(reservationPaymentResult.reservation()),
PaymentResponse.from(reservationPaymentResult.paymentResult()));
}
}
// ReservationApplicationService.java
**// [사전 예약 정보 & 결제 정보 DB 저장]**
**@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 6)**
public ReservationPaymentResult saveAdvanceReservationPayment(LoginMember loginMember, ReservationPaymentRequest reservationPaymentRequest) {
Reservation reservation = reservationService.saveAdvanceReservationPayment(loginMember, reservationPaymentRequest.toReservationRequest(), reservationPaymentRequest.toPaymentRequest());
PaymentResult paymentResult = paymentClient.purchase(reservationPaymentRequest.toPaymentRequest());
return new ReservationPaymentResult(reservation, paymentResult);
}
**// [상세 예약 정보 & 결제 정보 DB 저장]**
**@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 1)**
public ReservationPaymentResponse saveDetailedReservationPayment(
Reservation reservation,
PaymentResult paymentResult
) {
return reservationService.confirmReservationPayment(reservation, paymentResult);
}
각 트랜잭션에 timeout을 설정함으로써 비정상적인 지연 상황에서 자원 점유를 제한하고, 시스템 전체의 응답성을 유지할 수 있습니다.
데이터베이스에 여러 레코드를 삽입하고 외부 결제 시스템과 통신하는 등 상대적으로 시간이 소요될 수 있는 작업 →
timeout 6초
응답 지연 없이 빠르게 작업이 완료되어야 하는 작업 →timeout 1초
// RestClientConfig.java
// 커넥션 풀 매니저 생성 메서드
private PoolingHttpClientConnectionManager getPoolingHttpClientConnectionManager() {
// 커넥션 구성: **커넥션 생성 및 소켓 타임아웃을 설정**
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(connectionTimeOut)
.setSocketTimeout(socketTimeOut)
.build();
return PoolingHttpClientConnectionManagerBuilder.create()
.setDefaultConnectionConfig(connectionConfig)
.build();
}
// 요청 타임아웃 설정 메서드
private RequestConfig getRequestConfig() {
**// 응답 수신 시간 제한(read timeout)을 설정**
return RequestConfig.custom()
.setResponseTimeout(readTimeOut)
.build();
}
// HttpClient 생성 메서드
private CloseableHttpClient getHttpClient(PoolingHttpClientConnectionManager connManager, RequestConfig requestConfig) {
return HttpClients.custom()
.setConnectionManager(connManager) // 커넥션 풀 매니저를 사용하여 커넥션 재사용
.setDefaultRequestConfig(requestConfig) // 요청 관련 타임아웃 설정 적용
**// 재시도 전략 설정: 최대 3회 재시도, 1초 간격으로 재시도**
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, Timeout.ofSeconds(1)))
.build();
}
지연시간을 제한하기 위해 Connection Timeout, Socket Timeout, Read Timeout을 설정하여 정해진 응답 시간 내에 응답이 오지 않으면 호출이 실패처리하는 것으로 구성하였습니다. 또한, 일시적인 네트워크 오류나 서버 과부하로 인해 외부 API 호출이 실패할 경우를 대비하여, 지정된 횟수만큼 재시도하는 Retry 로직을 추가로 구성하였습니다.
ConnectionTimeOut: 3초
- 대부분의 시스템에서
InitRTO의 값이 1초로 설정되어 있다고 판단 (RFC 6298)Syn패킷 유실,Syn + Ack패킷 유실,Ack패킷 유실 과 같이 연결이 지연되어 실패하는 경우를 3가지로 판단하여 3초로 설정
ReadTimeOut: 30초
- 해당 부분은 토스 페이먼츠에 기술되어있는 것을 확인하여 30초로 구성
SocketTimeOut: 2초
- 패킷 하나가 유실되고 다시 재전송되기까지의 시간을 고려하여 구성

외부 API와 내부 DB 작업을 하나의 트랜잭션으로 묶게 되면, 외부 API 호출에 소요되는 지연 시간 동안 내부 DB 커넥션이 계속 점유되어 커넥션 고갈 문제가 발생할 위험이 있습니다. 또한, 사용자는 예약이 즉시 완료되기를 기대하지만, 이러한 지연으로 인해 응답 시간이 길어지면 사용자 경험이 크게 저하될 수 있습니다.
이를 방지하기 위해,
timeout을 설정함으로써 비정상적인 지연 상황에서 자원 점유를 제한하고, 시스템 전체의 응답성을 유지할 수 있습니다.
Connection Timeout, Socket Timeout, Read Timeout을 설정하여 각 단계의 지연 시간을 제한했습니다. 또한, 일시적인 네트워크 오류나 서버 과부하로 인해 외부 API 호출이 실패할 경우를 대비하여, 지정된 횟수만큼 재시도하는 Retry 로직을 추가로 구성하였습니다.