DB 커넥션 풀 완전 정복 — HikariCP 설정부터 풀 고갈 원인까지
DB 커넥션 풀, 왜 알아야 하나요?
애플리케이션이 DB에 쿼리를 한 번 보낼 때마다 TCP 연결을 새로 만들고, 인증하고, 세션을 준비한 뒤, 작업이 끝나면 바로 끊는다고 가정해 보겠습니다. 요청이 많아질수록 어떤 일이 생길까요?
- 요청마다 연결 생성 비용이 반복됩니다
- DB가 동시에 감당해야 할 연결 수가 급격히 늘어납니다
- 응답 시간이 흔들리고, 순간적으로 장애처럼 보이는 상황이 생깁니다
이 문제를 해결하기 위해 사용하는 것이 커넥션 풀(Connection Pool) 입니다. 커넥션 풀은 미리 연결을 만들어 두고 재사용해서, 연결 생성 비용을 줄이고 동시 요청을 더 안정적으로 처리하게 해줍니다.
이 글에서는 커넥션 풀이 왜 필요한지부터 시작해서, max pool size를 어떻게 이해해야 하는지, 풀 고갈(pool exhaustion)은 왜 생기는지, 그리고 HikariCP 기준으로 어떤 설정을 먼저 봐야 하는지까지 실무 관점에서 정리합니다.
Phase 1. 커넥션을 매번 새로 만들면 왜 비쌀까?
DB 커넥션은 단순히 "객체 하나 생성"이 아닙니다.
애플리케이션
↓
TCP 연결 생성
↓
DB 인증
↓
세션 준비
↓
쿼리 실행
↓
연결 종료
이 과정에는 네트워크 왕복, 인증 비용, DB 서버 리소스 할당이 모두 들어갑니다. 요청마다 이 작업을 반복하면 쿼리 자체보다 연결 준비 비용이 더 눈에 띄기 시작합니다.
커넥션 풀의 핵심 아이디어
커넥션 풀은 미리 여러 개의 DB 연결을 만들어 두고, 요청이 오면 그중 하나를 잠시 빌려 줍니다.
요청 A → 풀에서 커넥션 1 대여 → 사용 후 반납
요청 B → 풀에서 커넥션 2 대여 → 사용 후 반납
요청 C → 풀에서 커넥션 1 재사용
즉, 핵심은 새로 만드는 것보다 재사용하는 것입니다.
장점은 세 가지입니다
- 응답 시간 감소 — 이미 열린 연결을 바로 사용할 수 있습니다
- DB 보호 — 연결 수를 제한해서 DB가 감당 가능한 범위 안에서 동작하게 합니다
- 애플리케이션 안정성 향상 — 순간 트래픽에도 연결 생성 폭증을 줄일 수 있습니다
참고: 커넥션 풀은 성능을 무한히 늘려 주는 장치가 아닙니다. 더 정확히는 DB 연결을 제한된 자원으로 관리하게 해주는 장치입니다.
Phase 2. 커넥션 풀은 내부에서 어떻게 동작할까?
일반적인 요청 흐름은 다음과 같습니다.
1. 요청이 들어온다
2. 애플리케이션이 풀에 "커넥션 하나 주세요"라고 요청한다
3. 여유 커넥션이 있으면 즉시 대여한다
4. 쿼리 실행 후 커넥션을 닫는 대신 풀에 반납한다
5. 다른 요청이 그 커넥션을 다시 사용한다
여기서 중요한 것은 애플리케이션 코드에서 Connection.close()를 호출해도, 실제 물리 연결이 끊어지는 것이 아니라 풀로 반납된다는 점입니다.
풀이 관리하는 대표 상태
- idle connection — 지금은 사용 중이 아니지만 풀 안에서 대기 중인 연결
- active connection — 현재 누군가 빌려서 사용 중인 연결
- pending thread/request — 커넥션이 없어 대기 중인 요청
이 세 가지를 보면 대부분의 풀 문제를 해석할 수 있습니다.
Phase 3. max pool size는 무엇을 의미할까?
가장 많이 보게 되는 설정이 maximumPoolSize입니다. 이름은 익숙하지만, 의미를 오해하는 경우가 많습니다.
maximumPoolSize의 뜻
HikariCP에서 maximumPoolSize는 풀이 동시에 보유할 수 있는 최대 커넥션 수입니다.
예를 들어 maximumPoolSize = 10이면:
- 동시에 최대 10개의 요청만 DB 커넥션을 직접 사용할 수 있습니다
- 11번째 요청부터는 누군가 반납할 때까지 기다려야 합니다
즉, 이 값은 단순한 튜닝 숫자가 아니라 애플리케이션이 DB에 가하는 동시성 상한입니다.
크게 잡으면 무조건 좋을까?
아닙니다. 너무 크게 잡으면 오히려 나빠질 수 있습니다.
- DB의
max_connections를 빠르게 소모합니다 - 동시에 실행되는 쿼리가 많아져 DB CPU와 I/O 경합이 커집니다
- 느린 쿼리가 많을 때 문제를 숨긴 채 더 크게 폭발시킬 수 있습니다
커넥션 풀은 "클수록 좋다"가 아니라 DB가 감당 가능한 동시 실행 수에 맞게 제한해야 하는 자원입니다.
너무 작으면 어떤 일이 생길까?
- 요청이 풀에서 오래 대기합니다
- 애플리케이션 응답 시간이 급격히 증가합니다
- 결국
connection timeout예외가 발생합니다
즉, 너무 크면 DB가 힘들고, 너무 작으면 애플리케이션이 기다립니다. 커넥션 풀 튜닝은 이 둘 사이의 균형을 맞추는 작업입니다.
Phase 4. 풀 고갈(pool exhaustion)은 왜 생길까?
실무에서 가장 자주 만나는 문제는 커넥션 풀 고갈입니다. 보통은 "트래픽이 많아서"라고 생각하지만, 실제 원인은 더 구체적입니다.
1. 쿼리가 느려서 커넥션을 오래 붙잡는 경우
요청 10개가 각각 커넥션을 하나씩 사용 중
각 요청이 3초 동안 DB 작업 수행
maximumPoolSize = 10
새 요청은 최소 3초 가까이 대기
풀의 크기가 10이어도, 각 요청이 커넥션을 오래 점유하면 사실상 처리량은 크게 떨어집니다.
이때 문제의 본질은 "풀이 작다"가 아니라 커넥션 점유 시간이 길다는 것입니다.
2. 트랜잭션이 길어서 반납이 늦는 경우
트랜잭션 격리 수준, 데이터베이스 락, MVCC 글에서 반복해서 나온 것처럼, 긴 트랜잭션은 거의 항상 비쌉니다.
@Transactional
fun processOrder(orderId: Long) {
val order = orderRepository.findById(orderId)
val payment = externalPaymentApi.call(order)
order.complete(payment)
}
이 코드에서 외부 API 호출이 트랜잭션 안에 들어가 있으면, 그 시간 동안 커넥션도 함께 점유됩니다. 외부 API가 800ms만 걸려도 고트래픽 환경에서는 풀을 빠르게 잠식할 수 있습니다.
3. 커넥션을 반납하지 못하는 경우
프레임워크를 정상적으로 사용하면 드물지만, 저수준 JDBC 코드나 비정상 흐름에서는 커넥션 누수가 생길 수 있습니다.
Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
// 예외가 발생했는데 close가 호출되지 않음
이런 누수가 반복되면 active connection 수는 계속 증가하고, 결국 풀 전체가 고갈됩니다.
4. 애플리케이션 스레드 수와 풀 크기 균형이 맞지 않는 경우
서버 스레드는 200개인데 풀 크기가 10이라면, 이론적으로는 동시에 190개 요청이 대기할 수 있습니다. 이것 자체가 문제는 아니지만, DB 작업 비중이 높은 서비스라면 대기 시간이 빠르게 커질 수 있습니다.
중요한 것은 애플리케이션 스레드 수보다 커넥션 풀이 작을 수는 있지만, 그때 어떤 대기 모델을 의도하는지 알고 있어야 한다는 점입니다.
Phase 5. HikariCP에서 먼저 봐야 할 핵심 옵션
Spring Boot에서 가장 많이 쓰는 커넥션 풀은 HikariCP입니다. 옵션은 많지만, 처음부터 전부 볼 필요는 없습니다. 우선순위가 높은 것은 몇 가지뿐입니다.
maximumPoolSize
풀의 최대 크기입니다.
spring.datasource.hikari.maximum-pool-size=20
가장 먼저 봐야 하는 숫자지만, 이 값만 키워서 문제를 해결하려고 하면 대부분 실패합니다. 먼저 왜 커넥션이 오래 점유되는지를 확인해야 합니다.
minimumIdle
풀 안에 최소 몇 개의 idle 커넥션을 유지할지 결정합니다.
spring.datasource.hikari.minimum-idle=10
트래픽이 갑자기 올라올 때 미리 준비된 연결이 너무 적으면 초반 응답이 흔들릴 수 있습니다. 다만 HikariCP는 일반적으로 고정 크기처럼 운용하는 경우가 많아서, 실무에서는 minimumIdle을 따로 크게 조정하지 않는 경우도 많습니다.
connectionTimeout
커넥션을 빌리기 위해 최대 얼마나 기다릴지를 의미합니다.
spring.datasource.hikari.connection-timeout=3000
이 시간이 지나면 보통 이런 예외를 보게 됩니다.
Connection is not available, request timed out after 3000ms
이 예외가 보인다면 "DB가 죽었다"보다 먼저 풀이 바닥났거나, 커넥션 점유 시간이 너무 길다고 의심하는 편이 맞습니다.
maxLifetime
하나의 물리 커넥션을 얼마나 오래 유지할지 정하는 값입니다.
spring.datasource.hikari.max-lifetime=1800000
DB나 프록시가 먼저 연결을 끊기 전에 애플리케이션이 더 일찍 정리하게 맞추는 용도로 사용합니다. 이 값은 DB, 프록시, 로드밸런서의 idle timeout보다 조금 짧게 잡는 것이 일반적입니다.
idleTimeout
오랫동안 놀고 있는 idle 커넥션을 언제 정리할지 결정합니다.
spring.datasource.hikari.idle-timeout=600000
트래픽이 들쭉날쭉한 서비스에서는 너무 공격적으로 줄이면 연결 생성과 제거가 자주 반복될 수 있습니다.
Phase 6. 풀 크기는 어떻게 정해야 할까?
정답 공식 하나로 끝나지는 않지만, 방향은 분명합니다.
잘못된 접근
- CPU 코어 수만 보고 결정
- 서버 스레드 수와 동일하게 맞춤
- 장애가 나면 무조건
maximumPoolSize만 올림
이 방식은 겉으로만 버티게 만들고, 실제 병목은 그대로 숨겨 두는 경우가 많습니다.
더 현실적인 접근
- DB의 최대 허용 연결 수를 먼저 확인합니다
- 여러 애플리케이션 인스턴스가 그 연결 수를 어떻게 나눠 쓸지 계산합니다
- 평균 쿼리 시간과 피크 트래픽에서 active connection이 얼마나 필요한지 관찰합니다
- 대기 시간과 DB 부하를 함께 보면서 점진적으로 조정합니다
예를 들어 DB max_connections가 200이고, 앱 인스턴스가 4개라면 각 인스턴스가 50씩 다 쓰는 설계는 위험합니다. 운영자 세션, 배치, 관리자 툴, 예상치 못한 연결까지 고려하면 안전 여유분이 필요합니다.
중요한 관점
커넥션 수는 처리량의 원인이 아니라 결과인 경우가 많습니다.
- 쿼리가 빠르면 적은 커넥션으로도 많은 요청을 처리할 수 있습니다
- 쿼리가 느리면 많은 커넥션을 둬도 결국 DB에서 병목이 납니다
그래서 커넥션 풀 튜닝은 단독 작업이 아니라, 인덱스 기본, 인덱스가 안 타는 이유, 실행 계획 같은 쿼리 튜닝과 함께 봐야 합니다.
Phase 7. 장애 상황에서는 무엇부터 볼까?
커넥션 풀 문제는 증상만 보면 서버가 그냥 느린 것처럼 보이기도 합니다. 이럴 때는 아래 순서로 보면 좋습니다.
1. 풀 메트릭 확인
- active connection 수가 최대치에 붙어 있는가
- idle connection이 0에 가까운가
- pending request가 계속 쌓이는가
이 세 가지가 보이면 일단 풀 고갈 가능성이 높습니다.
2. 느린 쿼리 확인
- 특정 쿼리가 오래 걸리는가
- 락 대기 때문에 쿼리가 지연되는가
- 트랜잭션이 불필요하게 길지 않은가
풀 문제의 상당수는 결국 느린 쿼리 문제로 돌아갑니다.
3. 애플리케이션 코드 확인
- 트랜잭션 안에서 외부 API를 호출하는가
- 파일 I/O, Redis, HTTP 호출이 DB 트랜잭션 안에 들어 있는가
- 예외 상황에서 자원이 제대로 정리되는가
4. DB 한계 확인
- DB
max_connections에 근접했는가 - CPU, 디스크 I/O, lock wait가 급증했는가
애플리케이션 풀 크기만 키워도 되는 상황인지, 아니면 DB가 이미 한계인지 구분해야 합니다.
Phase 8. 실무에서 자주 하는 오해
오해 1. "커넥션 풀이 크면 무조건 빠르다"
아닙니다. 커넥션이 많아질수록 동시에 더 많은 쿼리가 DB로 밀려 들어갑니다. DB가 감당 가능한 범위를 넘기면 오히려 전체가 느려질 수 있습니다.
오해 2. "timeout 예외는 DB 장애다"
항상 그렇지는 않습니다. 실제로는 DB가 살아 있어도, 풀에 여유 커넥션이 없어서 애플리케이션이 기다리다가 timeout 나는 경우가 매우 많습니다.
오해 3. "풀 크기만 올리면 해결된다"
일시적으로 증상이 늦게 나타날 수는 있지만, 근본 원인이 느린 쿼리나 긴 트랜잭션이라면 결국 더 큰 규모로 다시 터집니다.
오해 4. "DB 연결은 많을수록 안전하다"
연결 수는 안전장치가 아니라 제한 장치에 가깝습니다. 너무 많은 연결은 보호가 아니라 과부하의 시작일 수 있습니다.
정리
- 커넥션 풀은 DB 연결을 재사용하고 제한하는 장치입니다 — 성능 향상보다도 자원 관리 관점이 더 중요합니다
maximumPoolSize는 애플리케이션이 DB에 거는 동시성 상한입니다 — 클수록 무조건 좋은 값이 아닙니다- 풀 고갈의 핵심 원인은 커넥션 점유 시간이 길다는 데 있습니다 — 느린 쿼리, 긴 트랜잭션, 외부 API 호출이 대표적입니다
- HikariCP에서는
maximumPoolSize,connectionTimeout,maxLifetime부터 봐야 합니다 — 숫자를 외우기보다 의미를 이해하는 것이 먼저입니다 - 풀 문제는 쿼리 튜닝 문제와 연결되어 있습니다 — 인덱스, 실행 계획, 락 대기를 함께 봐야 제대로 해결됩니다