들어가며: “커넥션 풀이 다 찼습니다” — 가장 흔한 운영 장애
Spring Boot 애플리케이션에서 HikariPool-1 - Connection is not available, request timed out after 30000ms 에러를 본 적이 있다면, 커넥션 풀 설정이 잘못되었거나 커넥션 누수(leak)가 발생한 것이다. HikariCP는 Spring Boot 2.0부터 기본 커넥션 풀이지만, 기본값만으로 운영하면 트래픽 증가 시 장애가 발생한다.
이 글에서는 HikariCP 공식 문서와 Spring Boot 공식 문서(Data Access Properties)를 근거로, 커넥션 풀 설정의 핵심 파라미터·장애 진단·Kubernetes 환경 최적화를 다룬다.
1. HikariCP 핵심 파라미터
1-1. 반드시 알아야 할 설정 7가지
| 파라미터 | 기본값 | 설명 | 권장 |
|---|---|---|---|
maximumPoolSize |
10 | 풀의 최대 커넥션 수 (active + idle) | 워크로드에 맞게 조정 (아래 공식 참고) |
minimumIdle |
=maximumPoolSize | 유지할 최소 유휴 커넥션 수 | maximumPoolSize와 동일 (HikariCP 권장) |
connectionTimeout |
30000 (30초) | 풀에서 커넥션을 얻기까지 최대 대기 시간 | 5000~10000ms |
idleTimeout |
600000 (10분) | 유휴 커넥션이 풀에서 제거되기까지 시간 | minimumIdle < maximumPoolSize일 때만 적용 |
maxLifetime |
1800000 (30분) | 커넥션의 최대 수명 | DB의 wait_timeout보다 2~3분 짧게 |
keepaliveTime |
0 (비활성) | 유휴 커넥션에 주기적 ping 전송 간격 | maxLifetime의 1/3~1/2 |
leakDetectionThreshold |
0 (비활성) | 커넥션 누수 감지 임계값 | 개발: 5000ms, 운영: 60000ms |
1-2. Spring Boot application.yml 설정
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20 # maximumPoolSize와 동일 (고정 풀)
connection-timeout: 5000 # 5초
idle-timeout: 600000 # 10분 (고정 풀이면 무시됨)
max-lifetime: 1740000 # 29분 (MySQL wait_timeout=1800 기준)
keepalive-time: 600000 # 10분마다 ping
leak-detection-threshold: 60000 # 60초 이상 반환 안 하면 경고
pool-name: app-pool
connection-test-query: SELECT 1 # JDBC4 드라이버면 불필요
2. maximumPoolSize 산정: “많을수록 좋다”는 오해
2-1. PostgreSQL/MySQL 공통 공식
HikariCP 공식 Wiki(About Pool Sizing)에서 인용하는 공식:
pool size = (core_count * 2) + effective_spindle_count
여기서 core_count는 DB 서버의 CPU 코어 수, effective_spindle_count는 디스크 스핀들 수(SSD는 보통 1로 계산)다.
예: 4코어 DB 서버 + SSD → (4 × 2) + 1 = 9. 대부분의 경우 10~20이면 충분하다.
2-2. 왜 커넥션이 많으면 오히려 느려지는가
| 커넥션 수 | 효과 |
|---|---|
| 적절 (10~20) | DB CPU/메모리 효율적 사용, 컨텍스트 스위칭 최소 |
| 과다 (50~100+) | DB 컨텍스트 스위칭 증가, 락 경합 심화, 오히려 처리량 감소 |
HikariCP 공식 문서는 명시적으로 “Pool size가 작을수록 성능이 좋다”고 경고한다. 600개 요청을 10개 커넥션 풀로 9,000ms에 처리하던 것을, 100개 커넥션 풀로 변경하면 36,000ms가 걸리는 벤치마크 결과도 제시한다.
2-3. 애플리케이션 인스턴스 수 고려
# 총 DB 커넥션 = 인스턴스 수 × maximumPoolSize
# 예: Pod 5개 × 20 = 총 100 커넥션
# MySQL max_connections (기본 151)을 초과하지 않는지 확인
Kubernetes에서 HPA로 Pod가 자동 확장되면, 최대 Pod 수 × maximumPoolSize가 DB의 max_connections를 초과할 수 있다. 이 계산을 반드시 사전에 해야 한다.
3. maxLifetime과 keepaliveTime: 좀비 커넥션 방지
3-1. 문제 상황
MySQL의 wait_timeout(기본 28800초=8시간)이 경과하면 DB가 유휴 커넥션을 강제 종료한다. 그런데 HikariCP는 이를 모르고 해당 커넥션을 애플리케이션에 제공한다. 결과: Communications link failure 에러.
3-2. maxLifetime 설정
HikariCP 공식 문서에 따르면, maxLifetime은 DB의 wait_timeout보다 최소 30초 이상 짧게 설정해야 한다. HikariCP가 DB보다 먼저 커넥션을 교체해야 좀비 커넥션이 발생하지 않는다.
# MySQL wait_timeout 확인
SHOW VARIABLES LIKE 'wait_timeout';
# → 28800 (8시간)
# HikariCP maxLifetime: 28800 - 60 = 28740초 = 28740000ms
# 또는 더 보수적으로 1800초(30분)로 설정
3-3. keepaliveTime (HikariCP 4.0+)
HikariCP 4.0(Spring Boot 2.7+)에서 추가된 keepaliveTime은 유휴 커넥션에 주기적으로 SELECT 1(또는 JDBC4 isValid())을 전송하여 커넥션이 살아있는지 확인한다.
# keepaliveTime은 maxLifetime보다 짧아야 함
# 권장: maxLifetime의 1/3 ~ 1/2
spring:
datasource:
hikari:
max-lifetime: 1740000 # 29분
keepalive-time: 600000 # 10분마다 ping
이 설정은 중간 장비(프록시, 방화벽)가 유휴 커넥션을 끊는 환경(특히 Kubernetes, 클라우드 NAT)에서 필수다.
4. 커넥션 누수(Leak) 감지와 디버깅
4-1. leakDetectionThreshold
spring:
datasource:
hikari:
leak-detection-threshold: 60000 # 60초
커넥션을 getConnection()으로 가져간 뒤 60초 안에 close()하지 않으면 HikariCP가 스택 트레이스와 함께 경고 로그를 출력한다:
WARN HikariPool-1 - Connection leak detection triggered for conn0,
stack trace follows
java.lang.Exception: Apparent connection leak detected
at com.example.service.OrderService.processOrder(OrderService.java:42)
...
이 스택 트레이스로 어디서 커넥션을 반환하지 않는지 정확히 파악할 수 있다.
4-2. 흔한 누수 패턴
| 패턴 | 원인 | 해결 |
|---|---|---|
| try 블록에서 예외 발생 | finally에서 connection.close() 미호출 | try-with-resources 사용 |
| @Transactional 밖에서 EntityManager 사용 | 영속성 컨텍스트가 닫히지 않아 커넥션 점유 | @Transactional 범위 내에서만 EntityManager 사용 |
| 트랜잭션 안에서 외부 API 호출 | 외부 API 응답 지연 → 커넥션 장기 점유 | 외부 호출을 트랜잭션 밖으로 이동 |
| OSIV(Open Session In View) 활성화 | 컨트롤러 응답까지 커넥션 점유 | spring.jpa.open-in-view=false |
5. Kubernetes 환경 특수 설정
5-1. Pod 종료 시 커넥션 풀 정리
Kubernetes Pod가 종료될 때 HikariCP가 열린 커넥션을 정상 반환해야 한다. Spring Boot의 Graceful Shutdown과 함께 설정한다:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
datasource:
hikari:
connection-timeout: 5000 # shutdown 중 새 커넥션 빠르게 실패
5-2. HPA + 커넥션 풀 용량 계산
# 시나리오: HPA max=10, maximumPoolSize=20
# 최대 DB 커넥션: 10 × 20 = 200
# MySQL max_connections: 최소 200 + 여유분(모니터링, 관리 접속)
# → max_connections = 250 이상 필요
# MySQL에서 확인
SHOW VARIABLES LIKE 'max_connections';
5-3. Cloud NAT/프록시 타임아웃
AWS NAT Gateway, GCP Cloud NAT 등은 유휴 TCP 커넥션을 일정 시간 후 끊는다(AWS: 350초). keepaliveTime을 이 값보다 짧게 설정해야 한다:
# AWS NAT Gateway idle timeout: 350초
# → keepaliveTime: 300000 (300초 = 5분)
spring:
datasource:
hikari:
keepalive-time: 300000
6. 모니터링: Actuator + Prometheus 연동
6-1. Spring Boot Actuator 메트릭
Spring Boot Actuator는 HikariCP 메트릭을 자동으로 노출한다:
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
주요 메트릭:
| 메트릭 | 설명 | 알람 기준 |
|---|---|---|
hikaricp_connections_active |
현재 사용 중인 커넥션 수 | maximumPoolSize의 80% 이상 → 경고 |
hikaricp_connections_idle |
유휴 커넥션 수 | 0이면 풀 고갈 임박 |
hikaricp_connections_pending |
커넥션 대기 중인 스레드 수 | 0보다 크면 즉시 대응 |
hikaricp_connections_timeout_total |
타임아웃 발생 누적 횟수 | 0보다 크면 풀 크기 또는 누수 확인 |
hikaricp_connections_max |
maximumPoolSize | — |
hikaricp_connections_usage_millis |
커넥션 사용 시간 히스토그램 | p95가 수 초 이상이면 쿼리 최적화 필요 |
6-2. Grafana 대시보드 핵심 패널
# Prometheus PromQL 예시
# 커넥션 풀 사용률
hikaricp_connections_active / hikaricp_connections_max * 100
# 대기 스레드 (0이어야 정상)
hikaricp_connections_pending
# 타임아웃 발생률 (분당)
rate(hikaricp_connections_timeout_total[5m]) * 60
7. 실전 체크리스트
- maximumPoolSize 산정: DB 코어 수 기반 공식으로 산정하고, (최대 Pod 수 × poolSize) ≤ max_connections인가?
- minimumIdle = maximumPoolSize: HikariCP 권장대로 고정 풀 크기를 사용하는가?
- maxLifetime < wait_timeout: DB의 wait_timeout보다 최소 30초 이상 짧게 설정했는가?
- keepaliveTime 설정: Cloud NAT/프록시 환경에서 유휴 커넥션 끊김을 방지하는가?
- leakDetectionThreshold: 개발 환경에서 커넥션 누수 감지를 활성화했는가?
- OSIV 비활성화:
spring.jpa.open-in-view=false로 불필요한 커넥션 점유를 방지했는가? - 모니터링:
hikaricp_connections_pending과timeout_total에 알람을 설정했는가?
8. 흔한 실수와 방지법
| 실수 | 증상 | 방지법 |
|---|---|---|
| maximumPoolSize을 100으로 설정 | DB 컨텍스트 스위칭 증가, 오히려 처리량 감소 | DB 코어 기반 공식으로 산정 (대부분 10~20) |
| maxLifetime > wait_timeout | 좀비 커넥션 → Communications link failure | maxLifetime = wait_timeout – 60초 |
| HPA 확장 시 max_connections 미고려 | Too many connections 에러 | (최대 Pod 수 × poolSize) + 여유분 ≤ max_connections |
| OSIV 기본값(true) 방치 | 컨트롤러 렌더링까지 커넥션 점유 → 풀 고갈 | spring.jpa.open-in-view=false 명시 |
정리
HikariCP 커넥션 풀 운영의 핵심은 “적은 커넥션을 효율적으로 순환”하는 것이다. maximumPoolSize는 DB 코어 기반으로 보수적으로 산정하고, maxLifetime과 keepaliveTime으로 좀비 커넥션을 방지하며, leakDetectionThreshold로 누수를 조기 탐지한다. Kubernetes 환경에서는 HPA 최대 Pod 수와 DB max_connections의 관계를 반드시 사전 계산하고, Actuator 메트릭으로 pending과 timeout을 모니터링하면 커넥션 풀 장애를 예방할 수 있다.