요청 1000개에 커넥션 1000개를 만들면?
트래픽이 급증하는 상황을 상상해보자. 매 요청마다 DB 커넥션을 새로 만들면 어떻게 될까.
DB 커넥션 생성 과정:
1. TCP 소켓 연결 (3-way handshake)
2. DB 서버 인증 (사용자/비밀번호 검증)
3. 세션 초기화 (인코딩, 타임존, 옵션 설정)
→ 수십 ~ 수백 ms 소요
→ 초당 1000 요청이면 커넥션 생성만으로 서버 과부하
커넥션 풀은 이 문제를 미리 만들어 두고 재사용하는 방식으로 해결한다.
HikariCP — Spring Boot의 기본 커넥션 풀
Spring Boot 2.0부터 HikariCP가 기본이다. “세상에서 가장 빠른 커넥션 풀"이라는 별명이 있다. 별도 설정 없이도 동작하지만, 기본값이 모든 상황에 맞지 않다는 게 함정이다.
동작 원리
애플리케이션 시작
→ 미리 커넥션 N개 생성해서 풀에 보관
HTTP 요청 처리
→ 풀에서 유휴 커넥션 꺼내기 (borrow)
→ DB 작업 수행
→ 커넥션 반납 (close가 아님, return)
풀이 비어있을 때
→ connectionTimeout 동안 대기
→ 초과 → SQLTimeoutException
핵심은 커넥션을 닫지 않고 반납한다는 것이다. 코드에서 connection.close()를 호출해도 실제로 닫히지 않고 풀로 돌아간다.
풀 사이즈 공식 — 너무 크면 오히려 느리다
직관적으로는 커넥션이 많을수록 좋을 것 같다. 하지만 커넥션이 너무 많으면 DB 서버 자체가 부하를 받는다.
HikariCP 공식 문서가 제안하는 공식:
maximumPoolSize = (CPU 코어 수 × 2) + 유효 디스크 수
예: CPU 4코어 + SSD 1개 = 9개
놀랍도록 작은 숫자처럼 보인다. 하지만 실제로 DB 연산은 CPU와 I/O를 번갈아 쓰므로, 코어당 2개가 적절하다. 그 이상은 Context Switch 오버헤드만 늘어난다.
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 10 # 최대 커넥션 수
minimum-idle: 5 # 유지할 최소 유휴 커넥션 수
connection-timeout: 30000 # 커넥션 대기 최대 시간 (ms)
idle-timeout: 600000 # 유휴 커넥션 유지 시간 (ms)
max-lifetime: 1800000 # 커넥션 최대 수명 (ms)
max-lifetime 주의사항
max-lifetime은 DB 서버의 wait_timeout보다 반드시 짧게 설정해야 한다.
DB 서버 wait_timeout: 8시간 (기본값)
max-lifetime: 30분 (권장)
→ DB가 연결을 끊기 전에 풀에서 먼저 커넥션을 교체
max-lifetime이 wait_timeout보다 길면, DB가 이미 닫은 커넥션을 풀이 계속 들고 있다가 사용 시점에 Connection is closed 에러가 난다.
커넥션 고갈 — 가장 흔한 장애 원인
풀의 모든 커넥션이 사용 중이면, 새 요청은 connectionTimeout까지 대기한다. 시간이 초과되면 에러다.
HikariPool-1 - Connection is not available, request timed out after 30000ms
커넥션 고갈의 주요 원인
1. 트랜잭션 안에서 외부 I/O
@Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId); // 커넥션 획득
sendEmail(order); // 이메일 전송 (3~5초)
// 이메일 전송하는 동안 커넥션을 계속 들고 있음
orderRepository.save(order);
} // 여기서 커넥션 반납
이메일 전송이 5초 걸리는 동안 커넥션 1개가 묶여 있다. 동시 요청이 많으면 풀이 금방 고갈된다.
해결: 트랜잭션 밖으로 느린 I/O를 분리한다.
public void processOrder(Long orderId) {
Order order = orderService.getOrder(orderId); // 짧은 트랜잭션
sendEmail(order); // 트랜잭션 밖
orderService.completeOrder(orderId); // 짧은 트랜잭션
}
2. 풀 사이즈가 너무 작음
요청 동시성 대비 풀이 작으면 대기 큐가 쌓인다. 모니터링으로 실제 사용량을 확인하고 조정해야 한다.
3. 긴 쿼리
인덱스 없는 쿼리가 10초씩 걸리면 그동안 커넥션이 묶인다. 쿼리 최적화로 해결한다.
모니터링 — Actuator로 풀 상태 보기
Spring Boot Actuator를 쓰면 커넥션 풀 상태를 실시간으로 볼 수 있다.
# application.yml
management:
endpoints:
web:
exposure:
include: health, metrics
GET /actuator/metrics/hikaricp.connections.active
GET /actuator/metrics/hikaricp.connections.pending
GET /actuator/metrics/hikaricp.connections.timeout
pending(대기 중)이 0이 아니거나 timeout이 발생하기 시작하면 풀 사이즈 조정 또는 쿼리 최적화를 검토해야 한다.
마치며
커넥션 풀은 조용히 잘 동작하다가 특정 임계점을 넘으면 갑자기 서비스 전체가 멈춘다. 평소에 maximum-pool-size, max-lifetime 설정을 확인하고, 트랜잭션 안에 느린 I/O가 없는지 리뷰하는 것만으로도 많은 장애를 예방할 수 있다.
다음 편부터는 심화 시리즈로, MySQL과 PostgreSQL의 내부 구조 차이를 다룬다.