-
DB 커넥션 풀 사이징: 왜 커넥션을 늘려도 TPS가 오르지 않을까? — 코어×2 공식과 리틀의 법칙
ARCHIVE 2026. 10. 24. 09:06반응형웹 서비스 트래픽이 급증하고 응답 지연이 발생할 때, 개발자가 가장 흔하게 저지르는 실수가 있다. WAS의 데이터베이스 커넥션 풀(Connection Pool) 크기를 10에서 50, 100, 200으로 무작정 늘리는 조치다. "일꾼이 많으면 밀린 일도 빨리 끝나겠지"라는 직관에 기댄다.
결과는 정반대로 흐른다. 커넥션 풀을 늘리자마자 데이터베이스 서버 CPU 사용률이 100%를 찍고, 쿼리 응답 시간은 급증하며, 시스템 전체가 마비에 빠진다.
커넥션 수가 늘어난다고 처리량(TPS)이 비례해 오르지 않는다. 오히려 특정 임계점을 넘는 순간 성능이 곤두박질친다. 이 역설을 이해하고 데이터베이스 하드웨어에 맞는 최적의 커넥션 풀을 산정하는 기준을 정리한다.
🔍 클릭하여 확대flowchart LR subgraph Client["요청 유입 (Request)"] R1[요청 1] R2[요청 2] RN[요청 N] end subgraph WAS["WAS Application"] Pool["Connection Pool<br/>(HikariCP)"] end subgraph DB["DB Server (8 Cores)"] C1["Core 1<br/>Worker"] C2["Core 2<br/>Worker"] C3["..."] C8["Core 8<br/>Worker"] Queue["Wait Queue"] end Client --> WAS Pool -->|적정 풀: 즉시 연산| DB Pool -.->|과도한 풀: 컨텍스트 스위칭 폭증| Queue
1. 커넥션이 늘어나면 시스템이 느려지는 이유 (Many ≠ Fast)
커넥션 풀은 연결 생성 비용을 줄인다. 클라이언트 요청마다 TCP 핸드셰이크, 인증, 세션 프로세스 생성을 반복하면 수십 밀리초(ms)가 낭비된다. 미리 연결을 맺어두고 재사용하는 풀링 기법은 필수다.
풀 크기가 무한정 커지면 하드웨어 한계에 부딪힌다. 8코어 CPU를 탑재한 데이터베이스 서버에서 커넥션을 200개 열면 어떤 일이 발생하는가? CPU 8개가 200개의 데이터베이스 프로세스(혹은 스레드)를 번갈아 수행해야 한다.
4가지 물리적 병목
- 컨텍스트 스위칭(Context Switching) 오버헤드: CPU 코어가 일 처리를 멈추고 다른 프로세스의 레지스터 상태를 교체하는 데 연산 자원을 탕진한다.
- CPU 캐시 무효화 (Cache Thrashing): 프로세스가 빈번하게 전환되면서 L1, L2, L3 CPU 캐시 라인이 반복해서 날아간다. 메모리 버스 접근이 폭증한다.
- 락(Lock) 경합 증가: 동시 실행 작업이 늘어나면 같은 데이터 행(Row)이나 테이블, 인덱스 래치(Latch)를 쥐기 위한 대기 시간이 길어진다.
- 버퍼 캐시 흔들림(Buffer Churn): 각 쿼리가 서로 다른 데이터 페이지를 요구하면서 버퍼 풀 적중률이 떨어지고 디스크 I/O가 쇄도한다.
HikariCP 벤치마크나 오라클 RWP(Real World Performance) 팀의 실측 데이터는 동일한 결론을 보여준다. PostgreSQL 환경에서 동시 활성 커넥션 수가 50개를 넘어서면 처리량 곡선이 평탄해지거나 급격히 하락한다. 오히려 커넥션을 줄였을 때 CPU 부하가 낮아지며 응답 속도와 전체 처리량이 개선된다.
유니버설 확장성 법칙 (Universal Scalability Law)
동시 처리 단위 $N$이 증가할 때 시스템 처리량이 선형으로 증가하지 못하는 현상은 닐 건터(Neil J. Gunther)의 USL 공식으로 설명된다.
$$X(N) = \frac{\lambda N}{1 + \alpha(N - 1) + \beta N(N - 1)}$$
- $\alpha$ (경합, Contention): 자원 공유로 인한 대기
- $\beta$ (일관성, Coherency): 분산 상태 동기화 및 캐시 일관성 비용
동시 커넥션 수 $N$이 커질수록 분모의 $N(N-1)$ 항(일관성 비용과 락 경합)이 기하급수적으로 커져 전체 처리량이 감소세로 꺾인다.
2. 경험적 시작점: 코어×2 공식
최적의 커넥션 풀 크기는 어디서부터 시작해야 하는가? PostgreSQL Wiki와 HikariCP 개발팀이 제시하는 경험 공식이 표준 기준점으로 쓰인다.
$$\text{connections} \approx (\text{물리 CPU 코어} \times 2) + \text{spindle\_count}$$
파라미터 정의 및 기준 물리 CPU 코어 하이퍼스레딩(논리 vCPU)을 제외한 순수 하드웨어 물리 코어 수 spindle_count 하드디스크(HDD) 회전축 수 (I/O 대기 중 다른 코루틴을 돌리기 위한 보정값) 현대 클라우드 인프라의 현실
오늘날 데이터베이스 서버는 회전판을 돌리는 HDD를 거의 쓰지 않는다. 고성능 NVMe SSD를 탑재하거나 대규모 RAM 버퍼 풀에 핫 데이터를 올려둔다.
- 디스크 회전축이 없으므로
spindle_count = 0으로 수렴한다. - 공식은 아래 형태로 단축된다.
$$\text{pool\_max} \approx \text{물리 CPU 코어} \times 2$$
8개 물리 코어를 가진 DB 인스턴스라면 커넥션 풀 16개 안팎이 출발점이다. 16코어 서버라면 32개 내외로 시작한다.
이 공식은 절대 불변의 법칙이 아니다. 쿼리가 디스크 I/O를 거의 유발하지 않는 순수 인메모리 연산 위주라면 물리 코어 수에 더 가깝게 줄여야 한다. 네트워크 호출이나 무거운 트랜잭션으로 대기가 길다면 코어당 3에서 5배까지 완만하게 확장하기도 한다. 오라클 RWP 가이드 역시 코어당 10 이하를 강력히 권고한다.
3. 병목의 본질 진단: 리틀의 법칙 (Little's Law)
커넥션 풀이 부족해서 대기가 발생하는지, 쿼리 자체가 느려서 풀이 마르는지 어떻게 구별하는가? 대기행렬 이론의 근간인 리틀의 법칙(John Little, 1961)을 적용한다.
$$L = \lambda \times W$$
- $L$: 시스템 내부의 평균 동시 작업 수 (Active Connections)
- $\lambda$: 단위 시간당 처리율 (Throughput / TPS)
- $W$: 작업의 평균 체류 시간 (Query Latency / MRT)
이 식을 데이터베이스 처리량(TPS) 관점으로 재정렬한다.
$$\text{TPS} = \frac{\text{Active Connections}}{\text{Query Time (초)}}$$
[ 시나리오 진단 예시 ] • DB 사양: 8 물리 코어 • 최대 커넥션 풀: 16 • 활성 커넥션(Active): 14 • 평균 쿼리 소요 시간: 50ms (0.05초) 이론적 최대 TPS = 14 ÷ 0.05 = 280 TPS이 상태에서 유입 트래픽이 400 TPS로 늘어났다고 가정하자. 커넥션 풀을 32개, 64개로 늘리면 해결되는가?
이미 8코어 CPU가 14개의 활성 연결을 처리하느라 한계에 달했다. 여기서 커넥션을 두 배로 늘리면 컨텍스트 스위칭과 락 경합 때문에 평균 쿼리 시간이 50ms에서 150ms로 3배 늘어난다.
$$\text{새로운 TPS} = \frac{32}{0.15} \approx 213 \text{ TPS}$$
풀을 늘렸더니 도리어 처리량이 280에서 213으로 떨어진다.
리틀의 법칙이 주는 진단 원칙은 분명하다. TPS를 높이는 유일하게 건강한 길은 분모인 Query Time을 줄이는 작업이다. 인덱스를 튜닝하고, 불필요한 테이블 풀 스캔을 제거하며, 캐시 계층을 도입해 쿼리 실행 시간을 50ms에서 5ms로 줄이면 동일한 14개 커넥션으로 2,800 TPS를 처리한다.
4. 멀티 WAS 환경의 치명적 함정: 총량 분배 공식
단일 서버가 아닌 여러 대의 WAS 인스턴스를 스케일아웃해 운영할 때 가장 많은 장애가 터진다.
"DB 서버가 8코어니까 커넥션 풀을 16으로 설정하자"고 결정한 뒤, WAS 4대에 각각
maximum-pool-size: 16을 적용하는 경우다.🔍 클릭하여 확대flowchart TD subgraph WRONG["잘못된 설정 (총 64개)"] WAS1["WAS 1 (Pool 16)"] WAS2["WAS 2 (Pool 16)"] WAS3["WAS 3 (Pool 16)"] WAS4["WAS 4 (Pool 16)"] end subgraph CORRECT["올바른 분배 (총 16~20개)"] WAS_A["WAS 1 (Pool 4)"] WAS_B["WAS 2 (Pool 4)"] WAS_C["WAS 3 (Pool 4)"] WAS_D["WAS 4 (Pool 4)"] end WRONG -->|총 64 커넥션 연결| DB1["DB (8 Cores) - 과부하"] CORRECT -->|총 16 커넥션 연결| DB2["DB (8 Cores) - 안정 연산"]데이터베이스 입장에서는 16개가 아니라 $16 \times 4 = 64$개의 커넥션이 맺어진다. 8코어 데이터베이스에 순식간에 과부하가 걸린다.
올바른 인스턴스 분배 계산
데이터베이스가 감당 가능한 총 풀 크기를 먼저 계산하고, 이를 WAS 대수로 나눠야 한다.
$$\text{WAS 1대당 Pool 크기} = \frac{\text{DB 물리 코어} \times 2}{\text{WAS 인스턴스 대수}}$$
8코어 DB와 4대의 WAS라면 인스턴스당 풀 크기는 4개가 이론적 상한이다. 특정 WAS에 트래픽이 일시 집중되는 편차를 감안하더라도 5에서 6개를 넘지 말아야 한다.
max_connections상한 준수PostgreSQL 기본
max_connections는 통상 100이다. 슈퍼유저 예약 및 백그라운드 워커를 감안하면 실제 애플리케이션 가용선은 80에서 90 이하다.$$\sum (\text{모든 WAS Pool 크기}) \le \text{max\_connections} \times 0.8$$
WAS가 수십 대 이상으로 늘어나는 대규모 마이크로서비스나 쿠버네티스 오토스케일링(HPA) 환경에서는 WAS 레벨의 커넥션 풀만으로 지탱하지 못한다. Pod가 50개로 늘어나는 순간 DB 커넥션 한도가 즉시 고갈된다.
이런 환경에서는 WAS와 DB 사이에 PgBouncer나 AWS RDS Proxy 같은 독립 커넥션 풀러(External Connection Pooler) 계층을 필수로 배치한다. 수천 개의 클라이언트 연결을 외부 풀러가 수용하고, DB 본체로는 코어 수에 맞춘 소수의 트랜잭션 연결만 흘려보낸다.
5. 실무 운영 체크리스트
커넥션 풀을 적정 크기로 조율한 뒤에도 프로덕션에서 발생하기 쉬운 5가지 운영 함정을 점검해야 한다.
1) 롱 트랜잭션 (Long-Lived Transaction) 차단
트랜잭션(
@Transactional) 블록 내부에서 외부 결제사 API를 HTTP로 호출하거나 복잡한 파일 I/O를 수행하면 해당 커넥션은 외부 통신이 끝날 때까지 아무런 DB 작업 없이 잠긴다. 풀이 순식간에 마른다. 외부 I/O는 트랜잭션 바깥으로 완벽히 격리한다.2) 타임아웃 불일치로 인한 좀비 커넥션
HikariCP의
maxLifetime(커넥션 최대 생존 시간, 기본 30분)이 네트워크 방화벽, L4 스위치, 클라우드 NAT 게이트웨이의 유휴 연결 차단 시간(예: 10분)보다 길면 문제가 발생한다. 방화벽이 이미 끊어버린 죽은 커넥션을 풀이 정상 연결로 착각하고 쿼리를 던지다가Connection Reset에러를 뿜는다.maxLifetime은 인프라 idle timeout보다 항상 2에서 3분 짧게 맞춘다.3) 스레드당 다중 커넥션 ($CM > 1$)과 데드락
하나의 HTTP 요청 스레드가 중첩 트랜잭션이나 별도 분기로 2개 이상의 DB 커넥션을 동시에 요구하면 커넥션 풀 데드락이 발생한다.
풀 크기가 작을 때 스레드 여러 개가 첫 번째 커넥션을 쥐고 두 번째 커넥션을 기다리며 상호 영구 대기에 빠진다. 스레드당 필요한 커넥션 수 $CM$이 1보다 크다면 아래 공식으로 최소 풀 크기를 진단한다.$$\text{Pool}_{\text{min}} = \text{스레드 수} \times (CM - 1) + 1$$
구조적으로 스레드당 단일 커넥션($CM = 1$) 원칙을 고수하는 편이 안전하다.
4) 누수 감지 활성화
누군가 커넥션을 획득하고
close()(반환)를 누락하면 풀이 점진적으로 고갈된다. HikariCP의leakDetectionThreshold를 30000ms(30초) 또는 60000ms(60초)로 설정한다. 일정 시간 이상 반환되지 않는 커넥션의 획득 스택트레이스를 즉시 로그로 출력해 누수 지점을 잡아낸다.5) 고정 풀 (Fixed Pool) 운영
minimumIdle을maximumPoolSize와 동일하게 맞춘다. 트래픽 유입에 따라 커넥션을 동적으로 생성하고 회수하는 비용을 없애고, 기동 시점에 DB 연결 한도를 확정해 운영 예측 가능성을 확보한다.반응형'ARCHIVE' 카테고리의 다른 글
MSA 분산 트랜잭션 설계의 기원: 1987년 Sagas 논문과 완화된 원자성 (0) 2026.10.15 카프카를 탄생시킨 핵심 설계 원칙: 2011 NetDB 논문과 초고처리량 아키텍처 (0) 2026.10.10 카프카(Kafka)를 메시지 큐로만 쓰면 안 되는 이유: 'The Log'와 분산 커밋 로그 아키텍처 (0) 2026.09.21