| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 스케줄러
- 이펙티브 자바
- DB
- Til
- spring boot
- 백엔드
- 로드밸런서
- java
- 코드카타
- GoF 23
- lv1
- 트러블슈팅
- 성능 개선
- 템플릿 메서드 패턴
- 토스
- Effective Java
- 추상클래스
- 디자인 패턴
- redis
- 빌더 패턴
- 자바
- 스프링 배치
- 프로그래머스
- 배치
- Spring Batch
- Spring
- 김영한
- 계산기
- 프록시 패턴
- 스프링
- Today
- Total
김코딩
[ 내돈 네돈 챌린지 ] 결제 API 성능 개선기: 트랜잭션 경계 재설계로 DB 커넥션 점유 시간 96% 단축 본문
들어가며
토스 페이먼츠 API를 연동한 결제 시스템을 개발하면서,
외부 API 호출로 인한 DB 커넥션 점유 문제를 발견하고 개선한 경험을 공유합니다.
트랜잭션 경계를 재설계하여 커넥션 점유 시간을 582ms에서 20ms로 단축했고,
이를 통해 시스템 안정성과 비용 효율성을 모두 확보할 수 있었습니다.
1. 문제 정의
토스 페이먼츠 API 연동 후, DB 커넥션 점유 시간이 비정상적으로 길다는 것을 발견했습니다.
부하테스트로 영향 확인
테스트 설정:

- 사용자: 50명
- Ramp-up period: 10 (10초동안 50명이 호출)
- 커넥션 풀: 5개
💡 커넥션 풀을 5개로 설정한 이유
HikariCP 기본값은 10개이지만, 테스트에서는 5개로 축소했습니다.
목적:
- "트래픽 대비 커넥션이 부족한 상황" 재현
- 트랜잭션 분리 효과를 명확히 보여주기 위함
실제로는:
- 평소 트래픽에서는 커넥션 10개로도 충분
- 하지만 이벤트/프로모션으로 트래픽이 급증하면 부족할 수 있음
- 소규모 서비스는 비용 절감을 위해 작은 RDS 인스턴스 사용 (권장 커넥션 5~10개)
테스트 결과:

- 평균 응답시간: 612ms
- Throughput: 4.8 req/sec
커넥션 5개로는 초당 최소 50건 이상 처리할 수 있어야 하는데,
실제로는 4.8건밖에 처리하지 못했습니다.
2. 문제 분석
애플리케이션 로그를 확인해봤습니다.

[결제 API] 전체 소요 시간: 600ms
[토스 API] 호출 시간: 569ms
[DB 작업] 저장 시간: 8ms
더 정확한 측정을 위해 트랜잭션 점유 시간을 로깅했습니다.
@Transactional
public PaymentConfirmResponse confirmAndChargePoint(Long userId, PaymentConfirmRequest request) {
long txStart = System.currentTimeMillis();
// 1. 회원 조회
userService.getProfile(userId);
// 토스 API 호출 시간 측정
long apiStart = System.currentTimeMillis();
TossConfirmResult tossConfirmResult = tossClient.confirmPayment(
request.getPaymentKey(), request.getOrderId(), request.getAmount());
long apiEnd = System.currentTimeMillis();
log.info("[토스 API] 호출 시간: {}ms", apiEnd - apiStart);
// Payment 객체 바로 생성
Payment payment = Payment.createSuccessPayment(
userId,
Money.of(request.getAmount()),
tossConfirmResult.getPaymentKey(),
tossConfirmResult.getStatus(),
tossConfirmResult.getMethod(),
tossConfirmResult.getApprovedAt().toLocalDateTime()
);
Payment savedPayment = paymentRepository.save(payment);
// 포인트 충전
pointService.chargePoint(
savedPayment.getUserId(),
savedPayment.getAmount().getValue(),
"CHARGE",
savedPayment.getOrderId().getValue()
);
long txEnd = System.currentTimeMillis();
log.info("[트랜잭션 점유] {}ms", txEnd - txStart);
return PaymentConfirmResponse.toDto(savedPayment);
}

[토스 API] 537ms
[트랜잭션 점유] 582ms ← 문제 발견!
트랜잭션이 582ms 동안 DB 커넥션을 점유하고 있었고,
그 중 537ms는 토스 API 응답을 기다리는 시간이었습니다.
-> 이 537ms 동안 커넥션은 아무 일도 하지 않고 대기만 하고 있었습니다.
Before 코드
@Transactional // 메서드 전체가 트랜잭션
public PaymentConfirmResponse confirmAndChargePoint(Long userId, PaymentConfirmRequest request) {
// 1. DB 조회 (커넥션 획득)
userService.getProfile(userId);
// 토스 결제 승인 API 호출 (500 ~ 600ms)
// 이 시간 동안 커넥션은 놀고 있음
TossConfirmResult tossConfirmResult = tossClient.confirmPayment(
request.getPaymentKey(), request.getOrderId(), request.getAmount());
// 결제 내역 저장
Payment payment = Payment.createSuccessPayment(
userId,
Money.of(request.getAmount()),
tossConfirmResult.getPaymentKey(),
tossConfirmResult.getStatus(),
tossConfirmResult.getMethod(),
tossConfirmResult.getApprovedAt().toLocalDateTime()
);
Payment savedPayment = paymentRepository.save(payment);
// 포인트 충전
pointService.chargePoint(
savedPayment.getUserId(),
savedPayment.getAmount().getValue(),
"CHARGE",
savedPayment.getOrderId().getValue()
);
return PaymentConfirmResponse.toDto(savedPayment);
} // 커넥션 반납

3. 해결 방안
3.1 트랜잭션 경계 재설정
문제의 핵심은 외부 API 호출이 트랜잭션 내부에 있다는 점이었습니다. 토스 API 응답을 기다리는 500 ~ 600ms 동안 DB 커넥션이 아무 일도 하지 않고 점유되고 있었습니다.
해결방법으로는 외부 API 호출을 트랜잭션 밖으로 빼는 것입니다.
After: 트랜잭션 분리
/**
* 결제 승인 및 포인트 충전
*/
// 메인 메서드: 트랜잭션 외부
public PaymentConfirmResponse confirmAndChargePoint(Long userId, PaymentConfirmRequest request) {
userService.getProfile(userId);
// 토스 API 호출 (실패 시 그냥 예외 전파)
TossConfirmResult tossConfirmResult = tossClient.confirmPayment(
request.getPaymentKey(), request.getOrderId(), request.getAmount());
try {
// DB 작업만 try-catch 처리
return processPaymentSuccess(request, tossConfirmResult);
} catch (Exception e) {
...보상 트랜잭션
}
}
// DB 작업만 짧은 트랜잭션
@Transactional
protected PaymentConfirmResponse processPaymentSuccess(PaymentConfirmRequest request,
TossConfirmResult tossConfirmResult) {
// 결제 성공 처리
Payment payment = markAsSuccess(tossConfirmResult, request.getAmount());
// 포인트 충전
pointService.chargePoint(
payment.getUserId(),
payment.getAmount().getValue(),
"CHARGE",
payment.getOrderId().getValue()
);
return PaymentConfirmResponse.toDto(payment);
}
개선점:
- 토스 API 호출은 트랜잭션 밖에서 -> 커넥션 점유 없음
- DB 작업만 짧은 트랜잭션으로 처리 -> 커넥션 점유 최소화
3.2 개선 효과 측정
Before(트랜잭션 분리 전):

-> [토스 API] 537ms
-> [트랜잭션 점유] 582ms
After(트랜잭션 분리 후):

-> [토스 API] 548ms
-> [트랜잭션 점유] 20ms
3.3 부하 테스트 결과
트랜잭션 분리 후에도 동일한 조건으로 Jmeter 부하테스트를 진행해보았습니다.
테스트 설정:

- 사용자: 50명
- Ramp-up period: 10 (10초동안 50명이 호출)
- 커넥션 풀: 5개
테스트 결과:

- 평균 응답시간: 611ms
- Throughput: 9.1 req/sec
| 항목 | Before | After | 개선율 |
| 커넥션 점유 시간 | 582ms | 20ms | 96.6% 📉 |
| Throughput | 4.8 req/s | 9.1 req/s | 89.6% 📈 |
4. 주의사항
트랜잭션을 분리하면 원자성이 깨집니다.
문제 시나리오:
1. 토스 결제 승인 성공 ✅
2. DB 저장 실패 ❌
3. 결과: "돈은 빠졌는데 포인트 안 들어옴"
해결 방안: Saga 패턴 기반 보상 트랜잭션 구현(다음 글에서 작성 예정)
마치며
핵심 정리
개선 효과:
- 커넥션 점유 시간 96.6% 감소 (582ms → 20ms)
- 비용 절감: 더 작은 DB 인스턴스로 운영 가능
- 장애 격리: 외부 API 문제가 전체 시스템을 멈추지 않음
- 트래픽 안전 마진 확보
적용 시점:
- 외부 API 호출이 있는 트랜잭션
주의사항:
- 트랜잭션 분리 시 원자성 깨짐
- 반드시 보상 트랜잭션으로 정합성 보장 필요
다음 글
- 결제 시스템 데이터 정합성 보장: Saga 패턴 구현기
- Resilience4j로 외부 API 장애 대응하기
'개발팁' 카테고리의 다른 글
| [ 내돈 네돈 챌린지 ] 외부 API 장애 전파 방지: Circuit Breaker & Retry 패턴 (0) | 2025.11.28 |
|---|---|
| [ 내돈 네돈 챌린지 ] 결제 시스템 데이터 정합성 보장: Saga 패턴 구현기 (0) | 2025.11.27 |
| 인덱스(Index)란 무엇인가? (0) | 2025.11.25 |
| [ 면접의 神 ] 동시 요청 환경에서 DB 병목 해결을 위한 캐싱 전략 수립 (0) | 2025.11.23 |
| 결제 시스템 성능 개선: 동기식에서 비동기식 처리로의 전환 (3) | 2025.08.06 |