증상: 이런 에러 보셨으면 이 글이 맞다
운영 서버에서 갑자기 API가 죽기 시작한다. 로그를 열면 이게 보인다.
Unable to acquire JDBC Connection
HikariPool-1 - Connection is not available, request timed out after 30000ms
(total=10, active=10, idle=0, waiting=23)
total=10, active=10, idle=0, waiting=23 — 커넥션 10개 전부 사용 중이고 23개 요청이 줄 서서 기다리고 있다는 뜻이다. 30초 기다리다가 포기한 것.
트래픽이 갑자기 몰릴 때, 아니면 슬로우 쿼리 하나가 터졌을 때 이 에러가 나온다. Spring Boot 기본 설정을 그대로 쓰다가 프로덕션에서 처음 맞닥뜨리는 경우가 많다.
원인 분류: 고갈의 원인은 크게 세 가지다
1. 풀 사이즈가 애초에 작다
Spring Boot HikariCP 기본값이 maximum-pool-size=10이다. 트래픽이 조금만 몰려도 금방 바닥난다.
2. 슬로우 쿼리로 커넥션이 묶인다
한 번에 처리돼야 할 쿼리가 인덱스 빠진 채로 5초씩 걸리면? 10개 커넥션이 전부 그 느린 쿼리 잡고 늘어진다. 새 요청은 줄 서서 기다리다 timeout.
3. 커넥션 누수 (Connection Leak)
코드에서 커넥션을 받아 쓰고 닫지 않은 경우. 특히 try-catch 안에서 예외 발생 시 close()를 빠뜨렸거나, @Transactional 없이 직접 커넥션 관리할 때 흔히 나온다. 커넥션이 풀로 돌아오지 않으니까 시간이 지날수록 고갈된다.
해결 방법
Step 1. 현재 풀 상태 먼저 파악
로그에서 풀 상태를 보려면 설정 추가:
# application.yml
logging:
level:
com.zaxxer.hikari: DEBUG
com.zaxxer.hikari.HikariConfig: DEBUG
출력되는 로그:
HikariPool-1 - Pool stats (total=10, active=10, idle=0, waiting=23)
MBean으로 실시간 모니터링도 가능:
spring:
datasource:
hikari:
register-mbeans: true
Step 2. 커넥션 풀 사이즈 조정
무조건 늘리는 게 답이 아니다. HikariCP 공식 권장 공식:
connections = (core_count × 2) + effective_spindle_count
SSD 서버 4코어라면: (4 × 2) + 1 = 9. 생각보다 작다. DB 서버가 받을 수 있는 최대 연결 수도 같이 고려해야 한다.
spring:
datasource:
hikari:
maximum-pool-size: 20 # 트래픽에 맞게
minimum-idle: 5 # 유휴 최소 유지 수
connection-timeout: 10000 # 30초 → 10초로 줄여서 빠르게 실패
idle-timeout: 300000 # 유휴 커넥션 유지 시간 5분
max-lifetime: 1800000 # 커넥션 최대 수명 30분
validation-timeout: 5000 # 커넥션 유효성 검사 타임아웃
leak-detection-threshold: 60000 # 60초 이상 반납 안 되면 누수 경고
connection-timeout을 30초에서 10초로 줄이는 게 핵심이다. 30초씩 기다리다 실패하면 그동안 요청이 쌓이고 상황이 더 나빠진다. 빨리 실패해서 로드밸런서가 다른 인스턴스로 트래픽 보내게 해야 한다.
Step 3. 슬로우 쿼리 잡기
커넥션 풀 늘려도 근본 원인이 슬로우 쿼리면 도로 막힌다.
spring:
jpa:
properties:
hibernate:
generate_statistics: true
logging:
level:
org.hibernate.stat: DEBUG
org.hibernate.SQL: DEBUG
-- MySQL 슬로우 쿼리 로그 활성화
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2; -- 2초 이상 쿼리 기록
-- 실행 중인 쿼리 확인
SHOW PROCESSLIST;
Step 4. 커넥션 누수 탐지
leak-detection-threshold 설정하면 누수가 있을 때 이런 로그가 찍힌다:
HikariPool-1 - Connection leak detection triggered for connection com.zaxxer.hikari.pool.ProxyConnection
스택 트레이스도 같이 출력되니까 어느 코드에서 커넥션을 안 닫고 있는지 바로 파악 가능.
// 잘못된 코드 — 예외 발생 시 close() 호출 안 됨
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...");
conn.close(); // 예외 터지면 여기 안 옴
// 올바른 코드 — try-with-resources 사용
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...")) {
// 처리
} // 자동으로 close() 호출
상황별 체크리스트
| 증상 | 원인 | 조치 |
|---|---|---|
| 트래픽 급증 시 발생 | 풀 사이즈 부족 | maximum-pool-size 증가 |
| 특정 기능 호출 후 발생 | 슬로우 쿼리 | 쿼리 최적화 / 인덱스 추가 |
| 서버 오랜 시간 운영 후 발생 | 커넥션 누수 | leak-detection-threshold 설정, 코드 점검 |
| 에러 응답이 30초 후에 옴 | connection-timeout 기본값 | 10초 이하로 줄이기 |
| DB 서버 부하 높음 | 풀 사이즈 과다 | 공식 기반으로 적정값 계산 |
실무에서 자주 놓치는 것
connection-timeout 줄이기를 무서워한다. "30초면 여유 있지 않냐"고 생각하는데 틀렸다. 요청이 30초씩 대기하면 그 사이에 더 많은 요청이 쌓인다. 10초 안에 실패 처리하고 클라이언트에 503 돌려주는 게 훨씬 낫다.
풀 사이즈를 무조건 크게 잡는다. DB 서버의 max_connections도 같이 봐야 한다. 애플리케이션 인스턴스가 3개인데 인스턴스당 커넥션 50개 잡으면 DB로는 150개 연결이 맺어진다. DB가 먼저 터진다.
개발환경과 운영환경 설정을 같이 쓴다. 개발 때는 minimum-idle=1, maximum-pool-size=5 정도로 작게 쓰고, 운영은 별도 프로파일로 관리하는 게 기본이다.
정리
HikariCP timeout은 대부분 풀 사이즈 부족, 슬로우 쿼리, 커넥션 누수 셋 중 하나다. 로그에 찍히는 (total=N, active=N, idle=0, waiting=M) 패턴 보고 원인부터 분류하고, connection-timeout은 줄여서 빠른 실패 전략 쓰고, 근본 원인을 잡는 순서로 접근하면 된다.
관련 글: Spring Boot UnexpectedRollbackException 해결 — 트랜잭션 롤백 에러 원인 분석
댓글 없음:
댓글 쓰기