DB 커넥션은 생성 비용이 매우 비쌉니다. TCP 핸드셰이크, TLS 협상, DB 인증, 세션 초기화까지 한 번 연결에 50~200ms가 소요됩니다. 커넥션 풀 없이 요청마다 연결을 새로 맺으면 P99 레이턴시가 폭발합니다. HikariCP가 왜 빠르고, 어떻게 설정하며, 실무에서 어떤 함정이 있는지 설명합니다.
SQLException| 설정 | 기본값 | 의미 | 실무 권장 |
|---|---|---|---|
maximumPoolSize |
10 | 최대 커넥션 수 (idle + active) | 가장 중요한 설정. 공식으로 계산 (하단 참조) |
minimumIdle |
maximumPoolSize와 동일 | 최소 유지 idle 커넥션 수 | HikariCP 공식 권장: maximumPoolSize와 같게 설정 (고정 풀 크기) |
connectionTimeout |
30000ms (30초) | 풀에서 커넥션 획득 대기 시간 | 30초는 너무 길다. 3~5초로 줄이면 빠른 실패로 시스템 보호 |
idleTimeout |
600000ms (10분) | 유휴 커넥션이 풀에서 제거되는 시간 | minimumIdle < maximumPoolSize일 때만 의미 있음 |
maxLifetime |
1800000ms (30분) | 커넥션 최대 수명. 초과 시 재생성 | DB 서버의 wait_timeout보다 반드시 짧게 설정! 안 하면 DB가 먼저 끊어서 "Connection closed" 에러 |
leakDetectionThreshold |
0 (비활성) | 커넥션이 이 시간 이상 반환 안 되면 누수로 경고 | 개발/스테이징 환경에서 5000ms 설정 권장. 누수 탐지 필수 도구 |
keepaliveTime |
0 (비활성) | 유휴 커넥션에 keepalive 쿼리를 보내는 주기 | DB 방화벽 타임아웃이 있는 환경에서 유휴 커넥션이 끊기는 것 방지 |
DB 서버는 각 커넥션을 별도 프로세스/스레드로 처리합니다. 커넥션이 100개라면 DB에서 동시에 100개 실행 컨텍스트를 유지해야 하고, CPU 코어가 4개라면 96개 컨텍스트는 항상 컨텍스트 스위칭 대기 상태입니다. 즉, 풀 크기를 늘릴수록 DB 서버의 CPU 컨텍스트 스위칭 오버헤드가 증가해 오히려 전체 처리량(Throughput)이 감소합니다. 실제로 HikariCP 벤치마크 결과, 풀 크기 9~10이 1000개보다 처리량이 높게 나오기도 합니다.
spring:
datasource:
hikari:
# "많으면 많을수록 좋겠지"
maximum-pool-size: 200
# 결과:
# - DB 서버 CPU 부하 급증
# - 컨텍스트 스위칭으로 전체 처리량 하락
# - DB 서버 max_connections 초과 시
# "too many connections" 에러
# - 커넥션 유지 비용(메모리)도 선형 증가
spring:
datasource:
hikari:
maximum-pool-size: 10
minimum-idle: 10 # 고정 풀 크기
connection-timeout: 3000 # 3초 빠른 실패
max-lifetime: 1800000 # 30분
# DB wait_timeout=3600s 이면
# max-lifetime < 3600s 로 설정 필수
keepalive-time: 60000 # 1분마다 keepalive
leak-detection-threshold: 5000 # 개발환경 누수 탐지
풀 크기가 작고 중첩 트랜잭션이 있을 때 데드락이 발생할 수 있습니다.
특히 @Transactional(propagation = REQUIRES_NEW)를 잘못 사용할 때 위험합니다.
@Service
public class OrderService {
@Autowired
private AuditService auditService;
@Transactional // 커넥션 #1 획득
public void placeOrder(Order order) {
processOrder(order);
// REQUIRES_NEW → 별도 커넥션 #2 요청
auditService.log(order); // 커넥션 #2 대기!
// maximumPoolSize=1이면 영원히 대기
}
}
@Service
public class AuditService {
@Transactional(
propagation = REQUIRES_NEW) // 새 커넥션 필요
public void log(Order order) { ... }
}
// 방법 1: maximumPoolSize 여유롭게 설정
// 중첩 TX 깊이만큼 여유 커넥션 확보
// max = 예상 동시 요청 수 + 중첩 깊이
// 방법 2: REQUIRES_NEW 사용 줄이기
// 감사 로그는 비동기로 분리
@Transactional
public void placeOrder(Order order) {
processOrder(order);
// 감사 로그는 이벤트로 발행 → 비동기 처리
eventPublisher.publishEvent(
new OrderPlacedEvent(order));
}
@Async // 별도 스레드, 별도 커넥션
@TransactionalEventListener
public void onOrderPlaced(OrderPlacedEvent e) {
auditRepository.save(e.getOrder());
}
커넥션이 반환되지 않고 계속 사용 중으로 남으면, 결국 풀이 고갈되어 전체 서비스가 먹통이 됩니다.
// @Transactional 없이 직접 커넥션 사용
public void badQuery() {
Connection conn = dataSource.getConnection();
// 예외 발생 시 conn.close() 호출 안 됨!
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...");
// 처리 중 예외 발생
processResult(rs); // ← 여기서 예외!
conn.close(); // ← 실행 안 됨 → 누수!
}
// 증상:
// leakDetectionThreshold 경고 로그:
// "Connection leak detection triggered for ..."
// 풀이 서서히 고갈 → 새벽에 서비스 중단
// 방법 1: try-with-resources (AutoCloseable)
public void safeQuery() {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("...")) {
processResult(rs);
} // 예외 발생해도 자동으로 close() 호출
}
// 방법 2: Spring @Transactional 사용
// 트랜잭션 종료 시 커넥션 자동 반환
@Transactional
public void safeService() {
// Spring이 커넥션 생명주기 관리
repository.findAll();
} // 커넥션 자동 반환
leakDetectionThreshold=5000(5초) 설정 시, 커넥션을 획득한 후 5초 이상 반환되지 않으면
HikariCP가 다음 경고 로그를 출력합니다:
WARN HikariPool-1 - Connection leak detection triggered for com.zaxxer.hikari...
스택 트레이스도 함께 출력되므로 어느 코드가 커넥션을 오래 점유하는지 바로 확인 가능합니다.
프로덕션에서는 느린 쿼리(30초 이상)가 오탐 가능하므로 쿼리 평균 실행 시간의 3~5배로 설정합니다.
| 최적화 항목 | 설명 |
|---|---|
| ConcurrentBag | 커넥션 관리용 자체 자료구조. ThreadLocal을 활용해 같은 스레드가 반환한 커넥션을 우선 재사용 (L1 캐시 효과) |
| Proxy 최소화 | JDBC의 Connection, Statement 프록시를 바이트코드 수준에서 직접 생성. 리플렉션 기반 프록시보다 월등히 빠름 |
| Invocation counting 제거 | 일부 JDBC 드라이버가 내부적으로 getAutoCommit()을 호출하는데, HikariCP가 이를 캐싱해 실제 DB 왕복 제거 |
| Pool suspending | DB failover 중 새 커넥션 생성 요청을 즉시 거부하지 않고 suspend 상태로 대기, DB 복구 후 자동 resume |
DB 서버는 각 커넥션을 별도 실행 컨텍스트로 처리합니다. 커넥션이 많아질수록 DB 서버에서 컨텍스트 스위칭이 증가하고, CPU 코어 수 대비 활성 커넥션이 많으면 오히려 전체 처리량이 감소합니다. 실제 HikariCP 벤치마크에서는 풀 크기 9~10이 1000개보다 TPS가 높게 나오는 결과도 있습니다. 또한 DB 서버의 max_connections를 초과하면 연결 자체가 거부됩니다. 부하 테스트를 통해 최적 크기를 측정하되, (CPU cores × 2) + 1을 초기 기준점으로 사용합니다.
maxLifetime을 DB 서버의 wait_timeout보다 길게 설정하면,
DB가 먼저 idle 커넥션을 끊어버립니다.
이미 끊긴 커넥션을 HikariCP가 유효하다고 생각하고 애플리케이션에 제공하면
"Connection is closed" 또는 "Communications link failure" 에러가 발생합니다.
maxLifetime은 DB의 wait_timeout보다 수 초~수분 짧게 설정해야 합니다.
또한 keepaliveTime을 설정하면 유휴 커넥션에 주기적으로 ping을 날려 DB가 끊는 것을 방지할 수 있습니다.
탐지 방법: leakDetectionThreshold를 설정하면 해당 시간 이상 반환되지 않는
커넥션에 대해 HikariCP가 경고 로그와 스택 트레이스를 출력합니다.
Prometheus의 hikaricp_connections_pending 지표가 꾸준히 증가하는 경우도 누수 신호입니다.
방지 방법: try-with-resources로 Connection.close()를 자동 보장하거나,
Spring @Transactional을 사용해서 스프링이 커넥션 생명주기를 관리하게 합니다.
장기 실행 배치 메서드에서 단일 트랜잭션으로 처리하면 커넥션을 오래 점유하므로
청크 처리로 나누는 것이 좋습니다.
커넥션 풀 크기는 많을수록 좋지 않다 — (CPU cores × 2) + 1을 기준으로 부하 테스트로 검증하고, maxLifetime은 DB wait_timeout보다 짧게, leakDetectionThreshold는 반드시 설정하라.