- Published on
Spring Boot HikariCP 커넥션 고갈 원인 8가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡히 떠 있는데도 어느 순간부터 요청이 줄줄이 느려지고, 로그에는 HikariPool-1 - Connection is not available, request timed out after ...ms가 반복된다면 거의 항상 “커넥션 풀 고갈” 시나리오입니다. 문제는 단순히 maximumPoolSize를 올려서 해결되는 경우가 드물다는 점입니다. 커넥션은 병목의 결과로 고갈되는 경우가 많고, 고갈을 유발한 원인을 제거하지 않으면 DB/네트워크/애플리케이션 전체가 연쇄적으로 무너집니다.
이 글에서는 Spring Boot + HikariCP에서 커넥션 풀이 고갈되는 대표 원인 8가지를 증상 → 진단 → 해결 순으로 정리합니다. 또한 EKS/RDS 환경에서 자주 섞여 들어오는 네트워크 타임아웃 이슈도 함께 다룹니다.
먼저 확인할 것: HikariCP 고갈의 전형적 신호
- 애플리케이션 로그
Connection is not available, request timed out after 30000msHikariPool-1 - Thread starvation or clock leap detected(원인이 다양하지만, 스레드/GC/시간점프 등으로 풀 대기시간이 비정상적으로 늘 때 관찰)
- 지표(Actuator/Micrometer)
hikaricp.connections.active가maximumPoolSize에 붙어서 떨어지지 않음hikaricp.connections.pending(대기)가 증가- DB의
Threads_running,max_connections근접
Spring Boot 2.5+ / 3.x에서는 Actuator로 Hikari 메트릭을 쉽게 노출할 수 있습니다.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
Prometheus에서 다음을 우선 봅니다.
hikaricp_connections_activehikaricp_connections_idlehikaricp_connections_pending
이제 원인 8가지를 보겠습니다.
1) 커넥션 누수(Connection Leak): close()가 보장되지 않음
증상
- 트래픽이 크지 않아도 시간이 지날수록
active가 계속 증가 - 재배포/재시작하면 잠시 정상 → 다시 악화
진단
- Hikari의 leak detection 활성화(운영에서는 값 조심)
spring:
datasource:
hikari:
leak-detection-threshold: 5000 # 5초 이상 반환 안 되면 경고
- 누수 로그에 스택트레이스가 찍히면 해당 코드 경로가 거의 정답입니다.
해결
- JDBC를 직접 쓰는 경우
try-with-resources로ResultSet/Statement/Connection을 확실히 닫습니다.
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement("select * from orders where id=?")) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
...
}
}
- Spring/JPA를 사용한다면 “커넥션을 직접 잡는 코드”가 섞여 있는지, 혹은 예외 경로에서 자원이 반환되지 않는지 확인합니다.
2) 트랜잭션이 너무 길다: @Transactional 범위가 과도함
증상
- 특정 API 호출 시 커넥션 점유 시간이 길고, 동시 요청이 늘면 급격히 고갈
- DB 락 대기와 함께 나타나기도 함
진단
- APM/슬로우 쿼리 로그에서 쿼리 자체는 빠른데 트랜잭션 시간이 긴 패턴
active는 높고idle이 거의 없음
해결
- 트랜잭션 범위를 “DB 작업”으로만 최소화합니다.
- 트랜잭션 내부에서 외부 호출(HTTP/S3/Kafka 등)이나 대용량 직렬화/파일 I/O를 하지 않습니다.
잘못된 예:
@Transactional
public void placeOrder(...) {
externalPaymentApi.call(); // 트랜잭션 안에서 외부 호출
orderRepository.save(order);
}
개선 예:
public void placeOrder(...) {
externalPaymentApi.call();
saveOrderInTx(...);
}
@Transactional
protected void saveOrderInTx(...) {
orderRepository.save(order);
}
3) N+1 / 과도한 쿼리로 커넥션 점유 시간이 증가
증상
- DB CPU/IO가 올라가고 응답이 점점 느려지며, 결국 풀 대기열이 증가
- 트래픽이 많아질수록 선형이 아니라 기하급수적으로 악화
진단
- Hibernate SQL 로그/통계에서 동일 패턴 쿼리 반복
- APM에서 한 요청이 수십~수백 쿼리를 발생
해결
- fetch join, batch size, DTO projection 등으로 쿼리 수를 줄입니다.
- 페이지네이션/정렬이 필요한 경우 쿼리 계획을 함께 점검합니다.
예: fetch join으로 N+1 완화
@Query("select o from Order o join fetch o.items where o.id = :id")
Optional<Order> findWithItems(@Param("id") Long id);
4) DB 락/데드락/긴 쿼리: 커넥션이 ‘대기’ 상태로 묶임
증상
- 특정 시간대에만 급격히 느려짐(배치/정산/인덱스 부재 등)
- DB에서 락 대기 증가, 슬로우 쿼리 증가
진단
- MySQL이라면
SHOW PROCESSLIST,performance_schema로 lock wait 확인 - PostgreSQL이라면
pg_stat_activity,pg_locks - 애플리케이션에서는 “커넥션 획득은 됐는데 쿼리가 끝나지 않는” 상태가 늘어남
해결
- 인덱스/쿼리 튜닝이 1순위
- 트랜잭션 격리 수준/업데이트 순서 일관화로 데드락 감소
- 애플리케이션에서 타임아웃을 명시해 무한 대기를 끊습니다.
JPA 쿼리 타임아웃 예:
Query query = entityManager.createQuery("select ...");
query.setHint("javax.persistence.query.timeout", 3000);
5) 풀 사이즈/스레드풀/요청 동시성 불일치
커넥션 풀은 “동시에 DB에 접근하는 작업 수”와 균형이 맞아야 합니다. 흔한 실수는 다음입니다.
- Tomcat/Jetty 요청 스레드가 너무 많음
- 비동기 Executor(예:
@Async) 스레드가 너무 많음 - 배치/스케줄러가 동시 실행되며 DB를 잠식
증상
- 트래픽이 조금만 늘어도
pending이 급증 - CPU는 여유 있는데 DB 대기만 늘어남
진단
server.tomcat.threads.max(또는 Undertow/Netty)와 HikarimaximumPoolSize비교- 비동기 작업이 DB를 사용하는지 확인
해결
- 풀을 무작정 키우기 전에 “동시 DB 작업 수”를 제한합니다.
예시 가이드(절대 법칙은 아님):
maximumPoolSize는 DB 인스턴스/워크로드에 따라 10~50 사이에서 시작- 웹 스레드가 200인데 풀 10이면 대기 폭발 가능
- 반대로 풀 200이면 DB
max_connections와 메모리/컨텍스트 스위칭 비용으로 장애
설정 예:
server:
tomcat:
threads:
max: 80
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 30000
6) 커넥션/소켓 타임아웃 부재: 네트워크 이슈가 풀을 잠가버림
EKS에서 RDS로 붙는 환경이라면 “DB가 느린 게 아니라 네트워크가 끊기거나 지연”되는 경우가 실제로 많습니다. 이때 애플리케이션은 커넥션을 잡은 채로 읽기/쓰기에서 블로킹될 수 있고, 그 결과 풀 고갈로 이어집니다.
증상
- 간헐적으로만 발생
- DB 지표는 크게 안 나쁜데 애플리케이션은 커넥션 대기
- RDS 쪽은 커넥션이 남아있는데 앱에서는 획득 실패
진단
- VPC/SG/NACL/NAT 경로 확인, 504/timeout 패턴 확인
- EKS→RDS 타임아웃을 빠르게 의심해야 할 때가 많습니다.
관련해서 네트워크 경로를 10분 안에 점검하는 체크리스트는 다음 글이 도움이 됩니다.
해결
- JDBC URL에 socketTimeout/connectTimeout 설정(드라이버별 상이)
- 쿼리 타임아웃과 함께 DB/네트워크 장애 시 빠르게 실패하도록 구성
- 커넥션 풀의
validationTimeout/keepaliveTime등을 적절히 사용(단, 과도한 keepalive는 DB 부하)
MySQL 예:
spring.datasource.url=jdbc:mysql://host:3306/app?connectTimeout=3000&socketTimeout=5000
7) 잘못된 커넥션 생명주기 설정: maxLifetime/idleTimeout 미스매치
HikariCP는 기본적으로 안정적이지만, 인프라(로드밸런서, NAT, 방화벽, RDS 프록시 등)가 유휴 커넥션을 더 짧은 시간에 끊어버리는 환경에서는 문제가 생깁니다.
- 풀은 살아있다고 생각하지만 실제로는 중간 장비가 커넥션을 끊음
- 다음 사용 시점에 예외가 나고, 재시도/대기가 겹치면 고갈처럼 보임
증상
- 특정 주기로
Communications link failure,Connection reset류 예외 - 예외 후 재시도가 겹치며
pending증가
진단
- 예외 발생 간격이 일정(예: 5분/10분/30분)
- 인프라 idle timeout(예: NAT idle timeout, 프록시 설정) 확인
해결
maxLifetime을 인프라 idle timeout보다 짧게 설정해 Hikari가 먼저 커넥션을 교체하도록 함
spring:
datasource:
hikari:
max-lifetime: 1740000 # 29분 (예: 30분 idle timeout보다 짧게)
keepalive-time: 300000 # 5분 (필요한 경우에만)
validation-timeout: 2000
8) 장애 시 재시도 폭발(Retry Storm)과 서킷브레이커 부재
DB 장애/네트워크 지연이 생겼을 때, 애플리케이션이 다음을 동시에 하면 커넥션 고갈은 매우 빨리 옵니다.
- 짧은 간격의 무제한 재시도
- 타임아웃이 긴 외부 호출/DB 호출
- 요청 유입은 계속됨(백프레셔 없음)
증상
- 장애가 시작되면 수 초~수 분 내에 풀 고갈
- 장애가 끝나도 한동안 회복이 느림(대기열/재시도 큐가 남아있음)
진단
- 재시도 라이브러리(Resilience4j/Spring Retry) 설정 확인
- 실패율이 올라갈 때 동시 요청 수가 줄지 않는지 확인
해결
- DB 접근 경로에 “빠른 실패”를 도입: 타임아웃, 제한된 재시도, 지수 백오프, 서킷브레이커
- 읽기 트래픽은 캐시/리드레플리카로 분산
Resilience4j 예(개념 코드):
@CircuitBreaker(name = "db", fallbackMethod = "fallback")
public Order findOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
public Order fallback(Long id, Throwable t) {
throw new ServiceUnavailableException("DB temporarily unavailable", t);
}
운영에서의 빠른 체크리스트(우선순위)
- pending이 증가하는가? (풀 고갈의 직접 신호)
- leak detection으로 반납 누락이 있는가?
- 트랜잭션/쿼리 시간이 긴가, N+1이 있는가?
- DB 락/슬로우쿼리/인덱스 부재가 있는가?
- 웹 스레드/비동기 스레드/배치 동시성이 풀 크기와 맞는가?
- EKS→RDS 네트워크 타임아웃/504 패턴이 있는가?
EKS에서 504/5xx가 섞여 보인다면 애플리케이션만 보지 말고 인그레스/네트워크 경로도 같이 점검해야 합니다.
마무리: “풀을 키우기”는 최후의 수단
HikariCP 커넥션 고갈은 대부분 (1) 커넥션을 오래 쥐고 있거나, (2) 너무 많은 동시 작업이 DB로 몰리거나, (3) 네트워크/DB 장애에서 빠르게 실패하지 못해 대기가 누적될 때 발생합니다. maximumPoolSize를 올리면 일시적으로는 좋아 보일 수 있지만, DB가 감당할 수 있는 동시 연결 수/쿼리 처리량을 넘기면 더 큰 장애로 이어집니다.
권장 흐름은 다음과 같습니다.
- 누수 여부 확인 → 트랜잭션 범위 축소 → 쿼리 수/시간 감소 → 락/인덱스 튜닝 → 동시성(스레드/배치) 제한 → 타임아웃/서킷브레이커로 장애 전파 차단
원하시면 현재 사용 중인 설정(application.yml의 Hikari/Tomcat 값), 트래픽 규모, DB 종류(RDS MySQL/Postgres), 증상 로그 일부를 기반으로 “원인 후보 우선순위”와 “권장 설정값 범위”를 같이 잡아드릴게요.