Skip to content

Payment Connection Pool Fix

silberbullet edited this page Aug 24, 2026 · 2 revisions

🔧 Payment — 커넥션 풀 순환대기 개선

커넥션 풀 순환대기 데드락을 고친 뒤, 같은 풀 크기에서 버그 유무에 따른 차이와 동시성 사다리(30→50→100)로 검증한 기록


목차

  1. 배경
  2. 검증 방법
  3. 풀=10, 버그 vs 수정 비교 (30 threads)
  4. 수정 버전 동시성 사다리 (30→50→100)
  5. 로컬 환경의 한계와 실무 환경 추정
  6. 결론

배경

Payment Load Test에서 찾은 커넥션 풀 순환대기 데드락의 원인은, confirm()의 지갑 충전 호출이 DB 트랜잭션 안에서 이루어진다는 것이었다. 지갑 충전은 gRPC로 지갑 서비스를 호출할 뿐 payment 쪽 로컬 DB 쓰기가 없으므로, 애초에 트랜잭션으로 감쌀 이유가 없었다 — 바로 위에 있는 Toss PG 호출과 같은 패턴으로 트랜잭션 밖으로 뺐다.

// Before — 트랜잭션 안에서 블로킹 gRPC 호출 (순환대기 유발)
paymentCommandRepositoryPort.transaction(() -> {
    walletCommandPort.increaseWalletBalance(...);
    return approvedPayment;
});

// After — Toss 호출과 동일한 패턴, 트랜잭션 밖에서 호출
walletCommandPort.increaseWalletBalance(...);

이 문서는 그 수정이 실제로 효과가 있는지 같은 조건에서 A/B로 검증하고, 수정 버전이 동시성을 얼마나 버티는지 사다리로 확인한 기록이다.


검증 방법

원칙 이유
워밍업 후 측정 재시작 직후 콜드 JVM 상태에서는 JIT·커넥션·gRPC 채널이 준비되지 않아 prepare(순수 DB insert)조차 실패할 만큼 결과가 왜곡된다. 재시작마다 5 threads로 짧게 예열한 뒤 측정했다.
풀 크기 고정 풀=10(Spring Boot 기본값)으로 고정해, "버그 있음"과 "수정함" 사이에 코드 차이 하나만 남도록 통제했다.

풀=10, 버그 vs 수정 비교 (30 threads)

버그 있음 수정 후
confirm 성공 78/106 (73.6%) 102/120 (85%)
평균 응답시간 8,669 ms 7,672 ms
p95 30,244 ms 30,267 ms
HikariCP 패턴 active·pending 완전 고정, 회복 없음 피크 찍고 자연스럽게 회복

Important

같은 풀 크기(10)에서 코드 한 줄 차이로 성공률이 73.6% → 85%로 올랐다. 이게 이번 수정이 "풀 크기 문제"가 아니라 구조적 순환대기를 고친 것이었음을 보여주는 핵심 증거다. 다만 수정 후에도 30 threads에서 15%는 여전히 실패한다 — 순환대기는 사라졌지만 풀=10 자체가 넉넉한 건 아니라는 뜻이다.

📸 버그 버전 증적 (2026-08-24 23:14:28 ~ 23:16:41)
HTTP 요청 수 (RPS) — 200 급감, 500 등장 ![버그-30-RPS](이미지1_여기)
평균 응답시간 — confirm 30초, prepare 20.8초에 고정 ![버그-30-응답시간](이미지2_여기)
HikariCP 커넥션 — 대기(pending) 30에 완전히 고정, 회복 없음 ![버그-30-HikariCP](이미지3_여기)
📸 수정 버전 증적, 30 threads (2026-08-24 23:26:00 ~ 23:27:45)
HTTP 요청 수 (RPS) — 200 위주, 500 소량 ![수정-30-RPS](이미지4_여기)
평균 응답시간 — 피크(confirm 30초 근접) 이후 9초대까지 회복 ![수정-30-응답시간](이미지5_여기)
HikariCP 커넥션 — 대기 30 플래토가 약 45초 후 자연스럽게 풀림 ![수정-30-HikariCP](이미지6_여기)

수정 버전 동시성 사다리 (30→50→100)

풀=10, 수정 버전 그대로 동시 접속 수만 올려가며 어디까지 버티는지 확인했다.

threads confirm 성공 성공률 비고
30 102/120 85% 위 비교와 동일 조건
50 60/93 64.5% 응답시간이 30초에서 회복 없이 끝까지 고정
100 53/136 39% 대기(pending)가 스레드 수만큼(100) 고정, 500 비중 확대

Note

100 threads의 HikariCP 그래프는 버그 버전(완전 정지)과 겉모습이 비슷하게 "평평"하지만, 성공률은 0이 아니라 39%다. 즉 완전히 멈춘 순환대기가 아니라 심하게 밀린 것 — 그래프 모양만으로는 이 둘을 구분할 수 없고, 성공률·처리량 같은 결과 지표를 반드시 함께 봐야 한다.

📸 50 threads 증적 (2026-08-24 23:27:45 ~ 23:29:58)
HTTP 요청 수 (RPS) ![수정-50-RPS](이미지7_여기)
평균 응답시간 — confirm·prepare 모두 30초 근처에서 끝까지 고정 ![수정-50-응답시간](이미지8_여기)
HikariCP 커넥션 — 대기 50 플래토 ![수정-50-HikariCP](이미지9_여기)
📸 100 threads 증적 (2026-08-24 23:29:20 ~ 23:31:48)
HTTP 요청 수 (RPS) — 500 비중이 눈에 띄게 커짐 ![수정-100-RPS](이미지10_여기)
평균 응답시간 — confirm·prepare 모두 30초대에 장시간 고정 ![수정-100-응답시간](이미지11_여기)
HikariCP 커넥션 — 대기가 스레드 수와 같은 100까지 치솟음 ![수정-100-HikariCP](이미지12_여기)

로컬 환경의 한계와 실무 환경 추정

이 모든 측정은 노트북 한 대(Intel i7-1260P, 12코어/16스레드, RAM 16GB) 위에서, 아래가 전부 동시에 떠 있는 상태로 진행됐다.

같이 떠 있던 것 비고
테스트 대상 애플리케이션 (JVM) IntelliJ로 실행
부하 생성기 (JMeter) 테스트 대상과 같은 CPU를 나눠 씀
PostgreSQL · Redis · MinIO · Prometheus · Grafana Docker Desktop WSL2 가상머신 하나를 공유 (메모리 한도 7.6GB)
IntelliJ, 브라우저, 이 작업 세션

Warning

정식 부하테스트라면 부하 생성기는 테스트 대상과 완전히 분리된 장비에서 돌리는 게 원칙이다. 여기선 노트북 한 대가 "재는 도구"와 "재는 대상" 역할을 동시에 하고 있어서, 지금 나온 절대적인 처리량 수치는 애플리케이션이 아니라 이 노트북의 한계일 수 있다.

그 밖에 실무와 다른 점:

  • application-local.ymlorg.hibernate.SQL: DEBUG, org.hibernate.type.descriptor.sql: TRACE 로깅이 켜져 있다 — 운영에서는 켜지 않는 설정이고, 쿼리마다 로깅 오버헤드가 붙는다.
  • PostgreSQL은 Docker 컨테이너 기본 설정 그대로다 (shared_buffers, max_connections 등 튜닝 안 됨).
  • Mock PG의 100ms 지연은 실측한 Toss 응답시간이 아니라 임의로 정한 값이다.
  • 앱 인스턴스가 1개뿐이다 — 실무라면 로드밸런서 뒤에 여러 대를 두는 게 기본이다.

그래도 바뀌지 않는 것

이번에 고친 버그(트랜잭션 안에서 블로킹 gRPC 호출)는 하드웨어와 무관한 구조적 순환대기다. 서버 스펙을 아무리 올려도 동시 confirm 수가 커넥션 풀 크기에 근접하는 순간 같은 순환대기가 재현된다 — 트리거되는 동시접속 규모의 문턱값이 올라갈 뿐이다. "서버 스펙이 좋으니 괜찮다"가 답이 될 수 없는 이유다.

모놀리스 구조의 부작용이기도 하다

payment와 wallet이 진짜 별도 서비스(별도 프로세스·별도 커넥션 풀)로 배포됐다면, payment가 커넥션을 쥐고 있어도 wallet 쪽 풀은 영향받지 않아 이 정확한 형태의 순환대기 자체가 발생할 수 없었다. 트랜잭션 안에서 외부 호출을 기다리는 습관 자체는 여전히 나쁘지만(HTTP 스레드 점유·지연은 남는다), 풀 전체가 마비되는 정도는 아니게 된다.

실무 환경이라면 달라질 것들

  • 커넥션 풀 크기 산정 — 무작정 키우지 않고 CPU 코어 수 기준으로 잡는다 (HikariCP 가이드: 코어 수 × 2 + 유효 스핀들 수가 출발점). 전용 튜닝된 Postgres 인스턴스라면 지금의 10~20보다 훨씬 큰 풀도 무리 없이 받아낼 가능성이 높다.
  • 수평 확장 — 인스턴스를 여러 대 띄우면 인스턴스당 감당할 동시 confirm 수가 줄어 전체 처리량은 인스턴스 수만큼 늘어난다 (다만 DB 자체가 새로운 병목이 될 수 있다).
  • 디버그 로깅 제거 + 부하 생성기 분리 — 지금 보이는 지연시간의 상당 부분이 걷힐 것이다.

Important

이 로컬 데이터만으로 "운영 환경에서는 TPS 몇까지 나온다"를 숫자로 예측할 수는 없다. 그러려면 운영과 동등한 스펙의 스테이징 환경에서 부하 생성기를 분리해 다시 측정해야 한다. 이번 테스트로 확실히 말할 수 있는 건 정성적인 결론이다 — 이 구조적 순환대기는 규모와 무관하게 존재했고, 풀 크기를 늘리는 것만으로는 고쳐지지 않는다는 것. 절대적인 처리량 숫자는 이 노트북 한 대에 대한 값일 뿐, 실무 용량 산정의 근거로 쓰면 안 된다.


결론

구분 내용
✅ 검증됨 같은 풀 크기(10)에서 코드 수정만으로 confirm 성공률 73.6% → 85%
✅ 검증됨 수정 후에는 부하가 늘어도 완전 정지가 아니라 점진적 저하 (30명 85% → 50명 64.5% → 100명 39%)
📌 방법론 HikariCP 그래프의 "평평함"만으로는 순환대기와 단순 과부하를 구분할 수 없다 — 성공률을 함께 봐야 한다
📌 방법론 콜드스타트와 정상 상태를 구분하려면 워밍업이 필수다
⚠️ 한계 절대적인 처리량 수치는 이 로컬 환경(노트북 한 대, 부하 생성기·DB·앱 자원 공유)에 한정된 값이다
🔭 향후 과제 풀=10에서도 여전히 15%는 실패 — 실무 배포 전 적절한 풀 크기 산정과 별도 스테이징 환경 재검증 필요

🚀 RAIO Backend

Home


📖 Getting Started

🏛️ Architecture

🧱 Core Modules

💳 Domains

📚 Guides

Clone this wiki locally