[JAVA 기술면접] DB Connection Pool — 사용 이유와 적정 크기
NOTE
질문: DB Connection이 무제한으로 늘어나는 것을 막으려고 Connection Pool을 쓰는데, 왜 쓰는가? 그리고 적절한 풀 크기는 어떤 기준으로 정하는가? 핵심은 두 가지다. ① 풀의 가치는 “개수 제한”뿐 아니라 연결 생성 비용 제거와 DB 보호다. ② 풀 크기는 “클수록 좋다”가 아니라 DB가 실제로 동시에 처리할 수 있는 양과 쿼리 점유 시간으로 정한다.
Q1. 왜 Connection Pool을 쓰는가?
1) 연결 생성 비용이 크다
요청마다 연결을 새로 만들면 매번 다음을 치른다.
TCP 3-way handshake → (TLS 핸드셰이크) → DB 인증/세션 초기화 → 쿼리 → 연결 종료
쿼리 자체는 1ms인데 연결 수립에 수 ms~수십 ms가 걸리기도 한다. 풀은 미리 만들어 둔 연결을 빌려 쓰고 반납하므로 이 비용을 없앤다.
2) DB 서버를 보호한다
- DB는 연결마다 자원을 쓴다. MySQL은 연결당 스레드, PostgreSQL은 연결당 프로세스를 두고 메모리(세션 버퍼 등)를 점유한다.
- DB에는
max_connections상한이 있다(MySQL 기본 151). 초과하면Too many connections로 신규 연결이 거절된다. - 풀이 없으면 트래픽 급증 시 연결 수가 폭증해 DB가 먼저 무너진다. 풀은 상한을 두고 초과분은 대기시켜 부하를 평탄화한다(대기열 역할).
3) 자원 관리를 한곳에서 한다
- 연결 재사용, 유효성 검사(죽은 연결 제거), 수명 관리, 누수 감지, 모니터링 지표를 풀이 제공한다.
- 애플리케이션은
getConnection()/close()(= 풀에 반납)만 하면 된다.close()는 실제 연결을 끊지 않고 풀로 되돌린다.
정리 표
| 풀 없음 | 풀 사용 | |
|---|---|---|
| 요청당 연결 | 매번 생성·종료 | 빌려 쓰고 반납 |
| 지연 | 연결 수립 비용 포함 | 즉시 사용 |
| DB 연결 수 | 요청 수에 비례해 폭증 가능 | 상한 고정 |
| 장애 시 | DB 다운으로 확산 | 대기/타임아웃으로 완충 |
Q2. 적절한 풀 크기는 어떤 기준으로 잡는가?
먼저 알아야 할 사실: “크다고 빠르지 않다”
DB는 CPU 코어와 디스크의 한계가 있어 동시에 실제로 실행할 수 있는 쿼리 수가 제한적이다. 연결이 그보다 많아지면 쿼리가 빨라지는 것이 아니라 컨텍스트 스위칭·락 경합·메모리 압박만 늘어 오히려 느려진다. 풀이 크면 스레드는 DB 앞에서 병목을 만나고, 풀이 작으면 애플리케이션 앞에서 대기한다. 어느 쪽이든 대기는 있고, 큰 풀은 그 대기를 DB 내부로 옮길 뿐이다.
기준 1) 경험식 (HikariCP 위키의 출발점)
connections = (DB 서버의 CPU 코어 수 × 2) + 유효 디스크 스핀들 수
- 예: 4코어 DB 서버 → 약 9~10개. 생각보다 작다.
- 이 식은 출발점이다. SSD·캐시 적중률이 높으면 디스크 대기가 줄어 더 작아도 충분하고, I/O 대기가 길면 더 필요할 수 있다.
- 기준은 애플리케이션 서버가 아니라 DB 서버의 코어다.
기준 2) 리틀의 법칙 (처리량과 점유 시간)
필요한 동시 연결 수 ≈ 초당 요청 수(TPS) × 연결 평균 점유 시간(초)
- 예: 500 TPS, 요청당 DB 연결을 평균 20ms(0.02초) 점유 → 500 × 0.02 = 10개.
- 점유 시간이 100ms로 늘면 같은 TPS에 50개가 필요하다. 즉 풀을 늘리기 전에 점유 시간을 줄이는 것(쿼리 튜닝, 트랜잭션 단축)이 먼저다.
- 여유율(피크, 지연 편차)을 곱해 보정한다.
기준 3) 전체 인스턴스 합이 DB 한도 안에 있어야 한다
(앱 인스턴스 수) × (인스턴스당 maximumPoolSize) + (배치·관리·모니터링 등 다른 접속) ≤ DB max_connections (여유 포함)
- 서버를 스케일 아웃하면 풀 크기를 그대로 둬도 DB 총 연결 수가 늘어난다. 오토스케일링 최대 대수 기준으로 계산한다.
- 한 DB를 여러 서비스가 공유하면 서비스별 할당량을 정한다.
- 연결 수가 너무 많아지면 DB 앞단에 프록시/풀러(ProxySQL, PgBouncer, RDS Proxy)를 둔다.
기준 4) 스레드 풀과의 균형
- 요청 스레드(Tomcat
maxThreads200)가 모두 DB를 쓰는 것은 아니다. 스레드 200개에 풀 10개여도 DB를 쓰는 시간 비율이 낮으면 충분할 수 있다. - 반대로 트랜잭션 안에서 외부 API 호출·파일 처리·
sleep을 하면 연결을 쥔 채 대기해 풀이 마른다. 트랜잭션 범위를 DB 작업만으로 좁힌다. - 비동기 작업·배치 스레드가 같은 풀을 공유하면 서로 굶길 수 있으므로 풀을 분리(읽기/쓰기, 온라인/배치)하기도 한다.
기준 5) 측정하고 조정한다
정답은 부하 테스트로 정한다.
| 지표 | 의미 | 신호 |
|---|---|---|
| Active 연결 수 | 사용 중인 연결 | 상시 최대치에 붙어 있으면 부족하거나 점유 시간이 김 |
| Pending(대기 스레드 수) | 연결을 기다리는 스레드 | 0이 아니면 풀 대기 발생 |
| Connection acquire 시간 | 풀에서 연결을 얻기까지 | 길면 풀 고갈 |
| Connection usage 시간 | 연결을 쥐고 있는 시간 | 길면 쿼리/트랜잭션 문제 |
| DB 쪽 활성 세션 수, CPU, 락 대기 | DB가 감당하는지 | 풀을 늘렸더니 DB CPU만 오르면 과도 |
조정 순서 권장: 느린 쿼리/트랜잭션 범위 개선 → 그래도 대기가 있으면 풀 증가 → DB 한도 근접하면 캐시·읽기 복제·프록시 도입.
HikariCP 주요 설정 (Spring Boot 기본 풀)
| 설정 | 기본값 | 설명과 권장 |
|---|---|---|
maximumPoolSize | 10 | 풀의 최대 연결 수. 위 기준으로 산정 |
minimumIdle | maximumPoolSize와 동일 | 유휴 최소 연결. 고정 크기 풀을 권장(수요 급증 시 연결 생성 지연 방지) |
connectionTimeout | 30초 | 풀에서 연결을 얻기 위해 기다리는 최대 시간. 초과하면 예외. 사용자 응답 시간 관점에서 더 짧게(예: 3~5초) 두는 경우가 많음 |
maxLifetime | 30분 | 연결의 최대 수명. DB/방화벽의 연결 종료 시간(wait_timeout 등)보다 수 초~수 분 짧게 설정해 끊긴 연결 사용을 방지 |
idleTimeout | 10분 | 유휴 연결 정리 시간(minimumIdle < max일 때만 의미) |
keepaliveTime | 0(비활성) | 유휴 연결에 주기적으로 핑을 보내 중간 장비의 끊김 방지 |
leakDetectionThreshold | 0(비활성) | 이 시간 이상 반납되지 않으면 누수 의심 로그(스택트레이스) |
validationTimeout | 5초 | 연결 유효성 검사 시간 제한 |
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20 # 고정 크기
connection-timeout: 3000 # ms
max-lifetime: 1740000 # 29분 (DB wait_timeout 30분보다 짧게)
leak-detection-threshold: 20000풀 고갈(Pool Exhaustion) 사례와 대응
| 원인 | 증상 | 대응 |
|---|---|---|
연결 누수 (close() 누락, 예외 시 반납 안 됨) | Active가 계속 최대, 점점 응답 불가 | try-with-resources, Spring 트랜잭션 사용, leakDetectionThreshold로 위치 추적 |
| 트랜잭션 안에서 외부 호출/긴 처리 | 연결 점유 시간 증가 | 트랜잭션 범위 축소, 외부 호출은 밖으로 |
| 느린 쿼리·락 대기 | 점유 시간 증가, 연쇄 대기 | 인덱스/쿼리 개선, 락 범위 축소, 쿼리 타임아웃 |
| OSIV(Open-In-View) 활성 | 컨트롤러·뷰 렌더링까지 연결 유지 | OSIV 비활성화, DTO로 변환 후 반환 |
| 트래픽 급증 | Pending 급증 | 캐시, 비동기 큐, Rate Limit, 서킷 브레이커로 DB 앞에서 완충 |
| 풀 크기 과대 + 인스턴스 증가 | DB Too many connections | 총합 계산, 프록시 도입 |
풀이 마르면
Connection is not available, request timed out after 30000ms같은 예외가 난다. 원인은 풀 크기 부족이 아니라 “누가 연결을 오래 쥐고 있나”인 경우가 대부분이다.
꼬리 질문 대비
| 질문 | 답변 요지 |
|---|---|
풀의 close()는 무엇을 하나? | 물리 연결을 끊지 않고 풀에 반납한다(프록시 래퍼가 가로챔). |
| 풀 크기를 무작정 늘리면? | DB 컨텍스트 스위칭·메모리·락 경합 증가로 오히려 느려지고, 총 연결 수가 max_connections에 닿아 장애가 날 수 있다. |
| 풀 크기가 너무 작으면? | 요청이 풀 대기열에서 기다려 지연이 늘고, connectionTimeout을 넘으면 실패한다. |
| 읽기/쓰기 분리 환경에서는? | 보통 Primary용·Replica용 풀을 따로 두고 읽기 전용 트랜잭션은 Replica 풀로 라우팅(AbstractRoutingDataSource). 각각 별도로 크기를 산정. |
| 왜 HikariCP가 빠른가? | 바이트코드 수준 최적화, 락 경합을 줄인 자체 컬렉션(ConcurrentBag), 불필요한 검사 최소화로 오버헤드가 낮다. Spring Boot 2.0부터 기본 풀. |
| 커넥션 풀과 스레드 풀의 차이? | 스레드 풀: 요청을 처리할 실행 단위 재사용. 커넥션 풀: DB 연결 재사용. 서로 다른 자원이지만 한쪽 병목이 다른 쪽에 영향을 준다(연결 대기 중인 스레드는 놀고 있다). |
| 풀 크기를 어떻게 검증하나? | 실제 트래픽 패턴으로 부하 테스트하며 Active/Pending/Acquire 시간과 DB CPU·활성 세션을 본다. 한 번에 한 값만 바꿔 비교. |
| 서버리스/오토스케일 환경의 주의점? | 인스턴스가 급증하면 총 연결 수가 폭증한다. 인스턴스당 풀을 작게, 또는 외부 프록시(RDS Proxy 등)로 연결을 다중화한다. |
maxLifetime을 왜 DB 타임아웃보다 짧게? | DB·방화벽이 먼저 연결을 끊으면 풀이 끊긴 연결을 내줘 Communications link failure 같은 오류가 난다. 풀이 먼저 갱신하도록 한다. |
한 줄 요약
- 왜: 연결 생성 비용을 없애고, DB의 연결 상한과 자원을 지키며, 초과 부하를 대기로 완충하기 위해.
- 크기:
DB 코어×2+스핀들을 출발점으로,TPS × 점유 시간으로 검증하고,인스턴스 수 × 풀 크기 ≤ DB 한도를 확인한 뒤, 측정으로 조정한다. 늘리기 전에 점유 시간부터 줄인다.