DB 커넥션을 미리 여러 개 맺어 두고 빌려주고 반납받는 구조. Spring Boot의 기본 구현은 HikariCP다.
왜 필요한가
커넥션 하나를 새로 맺는 비용
- = TCP 3-way handshake + 인증 + 세션 설정
- ≈ 수~수십 밀리초
요청마다 맺으면 그 자체가 병목이 된다
가장 자주 헷갈리는 것 — close()는 반납이다
Connection con = dataSource.getConnection();
// ...
con.close(); // 물리 연결이 닫히는 게 아니라 '풀로 반납' 된다
반납하지 않으면 커넥션 누수가 되고, 쌓이다 풀이 마르면 새 요청이 멈춘다.
// try-with-resources 로 예외가 나도 반드시 반납되게 한다
try (Connection con = dataSource.getConnection()) { ... }
spring:
datasource:
hikari:
leak-detection-threshold: 2000 # 2초 이상 안 돌려주면 경고 + 스택트레이스
connection-timeout: 3000 # 기본 30초는 너무 길다
maximum-pool-size: 20 # 기본 10
풀을 키우면 빨라지지 않는다
풀 100 → 300 으로 늘렸더니 더 느려졌다
이유: 커넥션이 많아지면 DB 쪽에서
- 동시 세션이 늘어 컨텍스트 스위칭이 증가
- 락 경합과 메모리 사용이 증가
- 처리량이 오히려 떨어진다
적정값은 DB 의 코어·디스크 수에 묶이는 작은 수다 흔한 출발점: (코어 수 × 2) + 디스크 수
풀 크기 이전에 쿼리를 빠르게 만드는 것이 먼저다. 느린 쿼리가 커넥션을 오래 쥐면 아무리 키워도 마른다.
고갈의 흔한 원인
- ① 트랜잭션 안에서 외부 API 호출 — 그동안 커넥션을 쥐고 있다
- ② OSIV 가 켜진 채 컨트롤러·뷰가 느리다
- ③ REQUIRES_NEW 중첩 — 요청당 커넥션 2개
- ④ 슬로우 쿼리
- ⑤ try-with-resources 없이 직접 JDBC 를 다뤘다
가상 스레드를 켜도 그대로다
스레드가 싸져도 커넥션 풀 크기는 그대로다
- 풀이 다음 병목으로 드러난다
- 풀을 키우는 게 아니라 connection-timeout 을 짧게 잡고
- 자원별 상한을 명시하는 방향이 맞다
면접 함정
- ❌ "풀을 키우면 동시 처리량이 는다" → 어느 지점을 넘으면 DB가 병목이 되어 반대가 된다.
- ❌ "close()하면 연결이 끊긴다" → 풀 환경에서는 반납이다.
지금 풀이 어떤 상태인지 본다
-
Actuator + Micrometer 가 HikariCP 지표를 자동 노출한다
- hikaricp.connections.active — 사용 중
- hikaricp.connections.idle — 놀고 있는
- hikaricp.connections.pending — 커넥션을 기다리는 스레드 수 ← 0이 아니면 경보
- hikaricp.connections.usage — 빌려 쓴 시간 분포
pending이 지속적으로 0보다 크면 풀이 부족하거나 어딘가 오래 쥐고 있다는 뜻이다. 응답 시간이 나빠지기 전에 잡히는 선행 지표다.
적정 크기를 계산해 본다
동시 처리해야 할 요청 수 × 요청당 커넥션 점유 시간 ÷ 목표 응답 시간
예) 초당 500 요청 · 요청당 커넥션 점유 10ms
- 500 × 0.01 = 5 (이론상 5개면 충분하다)
실제로는 여유를 두지만, 수백 개가 필요하다는 계산이 나온다면 점유 시간(느린 쿼리 · 트랜잭션 안의 외부 호출)을 먼저 의심한다
누수를 잡는 설정
spring:
datasource:
hikari:
leak-detection-threshold: 2000 # 2초 넘게 안 돌려주면 스택트레이스를 찍는다
max-lifetime: 1800000 # DB 의 wait_timeout 보다 짧게 (끊긴 커넥션 방지)
keepalive-time: 300000
max-lifetime을 DB의 유휴 타임아웃보다 짧게 잡지 않으면, DB가 먼저 끊은 커넥션을 풀이 계속 빌려줘 간헐적 오류가 난다.