데이터베이스 커넥션 풀 고갈 원인 분석: HikariCP 설정과 누수 탐지 방법

애플리케이션에서 데이터베이스 장애가 발생하면 가장 먼저 데이터베이스 서버의 CPU, 메모리, 디스크 I/O를 확인하는 경우가 많다. 그러나 실제 운영 환경에서는 데이터베이스 자체가 정상인데도 커넥션 풀 고갈로 인해 요청 처리가 지연되거나 서비스가 중단되는 사례가 적지 않다.
특히 Spring Boot에서 널리 사용하는 HikariCP는 가볍고 빠른 커넥션 풀이지만, 애플리케이션의 처리량과 데이터베이스 수용 능력을 고려하지 않고 설정하면 Connection is not available 오류가 반복될 수 있다. 단순히 최대 커넥션 수를 늘리는 방식은 일시적으로 증상을 완화할 뿐, 잘못된 트랜잭션 처리나 느린 SQL, 커넥션 누수 같은 근본 원인을 숨길 가능성이 크다.
이번 글에서는 데이터베이스 커넥션 풀이 고갈되는 주요 원인과 HikariCP 설정 기준, 커넥션 누수 탐지 방법을 운영 환경의 트러블슈팅 관점에서 정리한다.
커넥션 풀 고갈이 발생하는 구조
커넥션 풀은 애플리케이션이 데이터베이스 연결을 매번 새로 생성하지 않도록 일정한 수의 연결을 미리 만들어 관리하는 기능이다. 요청이 들어오면 풀에서 커넥션을 빌려 사용하고, SQL과 트랜잭션 처리가 끝나면 다시 풀에 반환한다.
정상적인 상황에서는 커넥션 대여와 반환이 짧은 시간 안에 반복된다. 하지만 모든 커넥션이 사용 중인 상태에서 새로운 요청이 들어오면 사용할 수 있는 커넥션이 반환될 때까지 대기해야 한다. 이 대기 시간이 HikariCP의 connectionTimeout을 초과하면 다음과 같은 오류가 발생한다.
HikariPool-1 - Connection is not available, request timed out
이 오류는 커넥션 수가 무조건 부족하다는 의미가 아니다. SQL 실행 시간이 길어졌거나, 트랜잭션이 종료되지 않았거나, 애플리케이션에서 커넥션을 반환하지 않는 상황에서도 동일하게 나타난다.
운영 중에는 애플리케이션 트래픽 증가, 데이터베이스 성능 저하, 잠금 경합, 외부 API 지연이 서로 영향을 주면서 풀 고갈이 발생하기도 한다. 예를 들어 데이터베이스 트랜잭션을 시작한 상태에서 외부 API 응답을 기다리면 API 지연 시간만큼 커넥션도 계속 점유된다. 평소에는 문제가 없지만 외부 시스템의 응답이 느려지는 순간 전체 커넥션 풀이 빠르게 소진될 수 있다.
HikariCP 주요 설정과 적정값 판단
HikariCP를 운영할 때 가장 먼저 확인해야 할 항목은 maximumPoolSize다. 이 값은 하나의 애플리케이션 인스턴스가 동시에 보유할 수 있는 최대 데이터베이스 커넥션 수를 의미한다.
spring:
datasource:
hikari:
maximum-pool-size: 30
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
keepalive-time: 300000
leak-detection-threshold: 60000
maximumPoolSize는 애플리케이션 인스턴스 한 대만 기준으로 계산해서는 안 된다. 쿠버네티스 환경에서 백엔드 Pod가 최대 30개까지 증가하고 각 Pod의 최대 풀 크기가 30이라면, 이론적으로 최대 900개의 데이터베이스 세션이 생성될 수 있다. 여기에 배치 서버, 관리자 도구, 모니터링 연결까지 포함하면 데이터베이스의 세션 한도를 초과할 수 있다.
따라서 전체 예상 커넥션 수는 다음과 같이 계산하는 것이 좋다.
전체 예상 커넥션 수 =
애플리케이션 최대 인스턴스 수 × 인스턴스별 maximumPoolSize
+ 배치 및 관리용 커넥션
+ 장애 대응 여유분
connectionTimeout은 풀이 고갈됐을 때 커넥션을 기다리는 최대 시간이다. 값을 지나치게 길게 설정하면 오류 발생은 늦어지지만 요청 대기열이 쌓여 서비스 응답 시간이 더 악화될 수 있다. 반대로 너무 짧으면 순간적인 부하에도 오류가 발생하므로 서비스의 응답 시간 기준과 데이터베이스 처리 시간을 함께 고려해야 한다.
maxLifetime은 커넥션을 풀에서 유지하는 최대 시간이다. 데이터베이스, 로드밸런서 또는 네트워크 장비의 연결 제한 시간보다 짧게 설정하는 것이 안전하다. 그렇지 않으면 이미 서버 측에서 끊어진 커넥션을 애플리케이션이 재사용하려 하면서 통신 오류가 발생할 수 있다.
idleTimeout은 사용하지 않는 유휴 커넥션을 정리하는 기준이며, keepaliveTime은 장시간 사용하지 않은 연결이 네트워크에서 끊어지는 것을 예방하는 데 활용된다. 다만 모든 설정값을 무조건 짧게 적용하면 커넥션 생성과 종료가 지나치게 빈번해질 수 있으므로 실제 세션 변화량을 확인하면서 조정해야 한다.
커넥션 고갈의 주요 원인과 확인 순서
첫 번째 원인은 느린 SQL이다. 평소 100밀리초에 끝나던 SQL이 실행계획 변경이나 데이터 증가로 인해 수 초 이상 걸리면 같은 처리량에서도 필요한 커넥션 수가 급격히 증가한다. 이때는 풀 크기를 늘리기 전에 데이터베이스의 활성 세션, SQL 실행 시간, 잠금 대기와 I/O 지연을 확인해야 한다.
두 번째 원인은 트랜잭션 범위가 지나치게 넓은 경우다. @Transactional 메서드 안에서 파일 처리, 네트워크 호출, 복잡한 연산을 수행하면 실제 SQL이 실행되지 않는 시간에도 커넥션이 계속 점유될 수 있다. 트랜잭션 범위에는 가능한 한 데이터베이스 작업만 포함하고, 외부 API 호출이나 파일 처리는 트랜잭션 외부로 분리하는 편이 좋다.
세 번째 원인은 커넥션 누수다. JDBC를 직접 사용하는 코드에서 Connection, Statement, ResultSet을 닫지 않거나 예외 처리 경로에서 반환 로직이 빠지면 커넥션이 풀로 돌아오지 않는다. try-with-resources를 사용하면 예외가 발생해도 리소스를 자동으로 정리할 수 있다.
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
// 조회 결과 처리
}
}
네 번째 원인은 데이터베이스 잠금 경합이다. 특정 행에 대한 UPDATE가 장시간 대기하면 해당 작업은 커넥션을 계속 점유한다. 동시에 같은 데이터를 변경하려는 요청이 증가하면 풀의 모든 커넥션이 잠금 대기 상태에 빠질 수 있다. 애플리케이션에서는 풀 고갈로 보이지만 실제 원인은 미완료 트랜잭션이나 비효율적인 갱신 순서일 수 있다.
점검할 때는 HikariCP의 활성 커넥션 수, 유휴 커넥션 수, 대기 중인 요청 수를 먼저 확인하고, 같은 시각의 데이터베이스 활성 세션과 대기 이벤트를 비교해야 한다. 애플리케이션 로그만 보면 커넥션 부족으로 판단하기 쉽지만, 데이터베이스 세션이 모두 잠금이나 I/O를 기다리고 있다면 데이터베이스 측 원인을 먼저 해결해야 한다.
누수 탐지와 운영 환경 트러블슈팅 방법
HikariCP의 leakDetectionThreshold는 일정 시간 이상 반환되지 않은 커넥션의 획득 지점을 로그로 남기는 기능이다. 예를 들어 값을 60초로 설정하면 커넥션을 빌린 후 60초 이상 반환하지 않은 코드 경로를 스택 트레이스로 확인할 수 있다.
다만 탐지 로그가 발생했다고 해서 반드시 실제 누수인 것은 아니다. 정상적으로 60초 이상 실행되는 배치 SQL도 누수 후보로 기록될 수 있다. 따라서 일반적인 온라인 요청의 처리 시간보다 충분히 큰 값으로 설정하고, 로그에 표시된 코드와 데이터베이스의 SQL 실행 상태를 함께 분석해야 한다.
실무에서 커넥션 풀 고갈을 경험했을 때 가장 효과적이었던 방법은 오류 로그 한 줄만 보는 것이 아니라 시간대별 지표를 나란히 비교하는 것이었다. HikariCP의 Active 커넥션이 최대치에 도달한 시각에 데이터베이스 CPU가 낮고 잠금 대기가 증가했다면, 풀 크기보다 미완료 트랜잭션을 의심할 수 있었다. 반대로 데이터베이스 CPU와 디스크 지연이 함께 상승하고 SQL 실행 시간이 늘었다면 느린 SQL이나 처리 용량 부족이 원인일 가능성이 높았다.
또한 애플리케이션이 오토스케일링되는 환경에서는 Pod 수가 늘어나는 순간 데이터베이스 연결도 급격히 증가한다. 서비스 부하를 처리하기 위해 Pod를 확장했지만 오히려 데이터베이스가 감당해야 할 동시 SQL 수가 증가해 성능이 더 나빠지는 상황도 발생할 수 있다. 이 경우 Pod별 풀 크기를 줄이고 전체 커넥션 예산을 관리하는 것이 단순 증설보다 효과적이다.
모니터링 항목으로는 Active, Idle, Pending, Max 커넥션 수와 커넥션 획득 시간을 권장한다. Pending 값이 지속적으로 증가한다면 풀 내부에서 요청이 대기하고 있다는 의미다. 여기에 데이터베이스의 활성 세션 수, 장기 실행 SQL, 잠금 대기, 트랜잭션 유지 시간, CPU와 I/O 지표를 같은 시간축으로 확인하면 원인을 빠르게 좁힐 수 있다.
설정을 변경한 뒤에는 반드시 부하 테스트를 수행해야 한다. 평균 응답 시간만 확인하지 말고 최대 트래픽에서 커넥션 대기 시간과 데이터베이스 부하가 어떻게 변하는지 살펴봐야 한다. 풀 크기를 늘린 후 애플리케이션 오류는 줄었지만 데이터베이스 CPU와 잠금 경합이 증가한다면 올바른 해결책이라고 보기 어렵다.
마치며
데이터베이스 커넥션 풀 고갈은 단순히 maximumPoolSize가 작아서 발생하는 문제가 아니다. 느린 SQL, 장시간 트랜잭션, 커넥션 누수, 잠금 경합, 외부 API 지연, 오토스케일링에 따른 세션 급증 등 다양한 원인이 같은 증상으로 나타난다.
운영 환경에서는 먼저 HikariCP의 Active, Idle, Pending 지표를 확인하고 같은 시간대의 데이터베이스 세션과 대기 상태를 비교해야 한다. 이후 누수 탐지 로그와 애플리케이션 스택 트레이스를 이용해 커넥션을 오래 점유하는 코드 경로를 추적하는 방식이 효과적이다.
개인적으로는 풀 크기를 먼저 늘리는 방식보다 커넥션이 어디에서 얼마나 오래 사용되는지를 확인하는 접근이 문제 해결에 더 도움이 됐다. 커넥션 수 증가는 장애 시점을 늦출 수 있지만 느린 SQL이나 잘못된 트랜잭션 구조를 해결해 주지는 않는다. 애플리케이션 인스턴스 수와 데이터베이스의 처리 능력을 기준으로 전체 커넥션 예산을 정하고, 모니터링과 부하 테스트를 통해 적정값을 검증해야 안정적인 서비스를 운영할 수 있다.




댓글 0
첫 댓글을 남겨보세요.