Connection Pool 고갈 증상
Connection Pool은 애플리케이션과 데이터베이스간의 물리적인 tcp 커넥션을 미리 일정개수 만큼 생성해두고 클라이언트 요청이 올대마다 이를 빌려주고 반납받는 캐싱 기법이다.
spring boot에서는 주로 HikariCP가 기본 구현체로 사용된다.
데이터베이스가 커넥션을 맺는 과정 tcp 3way handshake등은 컴퓨팅 리소스와 시간이 많이 소요된다. 풀링 기법은 이 오버헤드를 제거해 시스템의 처리량을 높인다.
Connection Pool 고갈은 풀에 설정된 최대 커넥션의 수가 모두 사용중(active)인 상태에서 새로운 데이터베이스 접근 요청이 인입되면 발생한다.
새로운 요청은 커넥션을 할당받기 위해 설정된 대기시간 connection timeout동안 queue에서 블로킹 상태로 대기하게 된다.
Problem Definition #
커넥션 풀 고갈은 단순히 데이터베이스 쿼리의 실패로 끝나지 않고 애플리케이션 서버 전체 장애로 확장되는 특성을 가진다.
WAS Thread Pool 고갈: 톰캣과 같은 웹 애플리케이션 서버 was의 워커 스레드가 db connection을 얻기 위해 waiting 상태에 빠진다.
커넥션 반납이 지연되면 가용한 WAS 스레드 마저 모두 고갈되어 db접근이 필요없는 단순한 헬스체크나 정적 리소스 요청조차 503 service unavailable에러를 반환한다.
레이턴시도 급증하는데 대기열에 진입한 요청들은 쿼리 실행 시간에 커넥션 획득 대기시간이 더해지므로 전반적인 api 응답 지연이 발생한다.
deadlock 위험도 존재한다 하나의 트랜잭션 내에서 여래개의 커넥션을 요구하는 로직이 있다면 풀 사이즈가 작을때 스레드들이 서로 커넥션을 점유한 상태로 남은 커넥션을 요구하며 영구적인 교착상태에 빠질 수 있다.
Example #
애플리케이션의 max pool size가 10으로 설정되어 있다고 치자.
각 쿼리의 평균 소유시간은 10ms이다. 그러면 하나의 커넥션이 초당 100건을 처리할 수 있으므로 풀 전체가 초당 1000건 (100 x 10)의 요청을 처리할 수 있다.
신규 배포된 기능에서 인덱스가 누락된 select 쿼리가 발생했다 table full scan이라서 쿼리 소요 시간이 5초로 증가되면 동시에 10개 요청이 인입되어 10개의 커넥션을 모두 점유하고
이 커넥션들은 결국 5초동안 반납되지 않아 추가 인입되는 수백개의 정상적인 요청들은 커넥션을 얻지 못하고 대기열에 쌓인다.
기본적으로 설정된 connection timeout ex 30s 경과하면 SQLTransientConnectionExecption이 동시다발적으로 발생해 시스템 장애로 이어진다.
해결방법 #
장애 발생시 애플리케이션 thread dump나 로그를 통해 커넥션 대기 상태를 증명하고 코드레벨에서 커넥션 점유시간을 최소화 해야한다.
WAS의 pid 기준으로 jstack을 통해 스레드들이 커넥션 풀에 대기하고있는지 확인한다
# 1. Java 프로세스의 스레드 덤프를 추출하여 HikariCP 관련 대기(WAITING) 상태의 스레드 수를 집계
$ jstack $(pgrep -f "spring-boot-app.jar") | grep "HikariPool" | grep "WAITING" | wc -l
# 2. 애플리케이션 로그에서 커넥션 획득 실패(Timeout) 에러 발생 확인
$ grep -A 2 "Connection is not available" /var/log/app/application.log | tail -n 5
# jstack 결과 (대기 중인 스레드 수)
154
# 로그 grep 결과
2026-03-09 22:45:10.123 ERROR [http-nio-8080-exec-15] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Connection is not available, request timed out after 30000ms.
2026-03-09 22:45:10.123 ERROR [http-nio-8080-exec-15] org.hibernate.exception.JDBCConnectionException : Unable to acquire JDBC Connection
결과분석: 154개의 웹 요청 스레드가 커넥션을 기다리며 블로킹되어 있으며, 30초의 TIMEOUT이 발생해 커넥션 풀이 완전히 고갈되어있음을 데이터로 확인했다.
트랜잭션의 경계 분리를 통한 Kotlin 방어 코드 구현을 해보겠다.
커넥션 풀 고갈의 가장 큰원인은 트랜잭션 블록 내부에서 외부 api 호출같은게 발생해 불필요하게 db커넥션을 오래잡고있는 안티패턴들 때문인데 이를 퍼사드 패턴으로 분리해야한다.
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.slf4j.LoggerFactory
// [안티 패턴]: 외부 API 호출 시간 동안 DB 커넥션을 불필요하게 점유함
@Service
class BadOrderService(
private val orderRepository: OrderRepository,
private val paymentClient: PaymentClient
) {
@Transactional
fun processOrderBad(orderId: Long) {
// 1. 커넥션 획득 및 로직 수행
val order = orderRepository.findById(orderId)
// 2. 문제 발생 구간: 외부 네트워크 통신 (예: 3초 소요)
// 이 3초 동안 DB 커넥션은 아무 작업도 하지 않으면서 반납되지 않고 유지됨
paymentClient.pay(order.amount)
// 3. 상태 변경 및 트랜잭션 커밋, 커넥션 반납
order.complete()
}
}
// [해결 방안]: 트랜잭션 경계를 최소화하여 DB 커넥션 점유 시간을 단축
@Service
class GoodOrderFacade(
private val orderQueryService: OrderQueryService,
private val paymentClient: PaymentClient,
private val orderCommandService: OrderCommandService
) {
private val log = LoggerFactory.getLogger(this::class.java)
// @Transactional 어노테이션을 상위 메서드에서 제거함
fun processOrderGood(orderId: Long) {
// 1. 필요한 데이터만 짧은 트랜잭션으로 조회 (커넥션 즉시 반납)
val orderAmount = orderQueryService.getOrderAmount(orderId)
// 2. 외부 API 호출 (DB 커넥션을 점유하지 않은 상태에서 실행됨)
// 외부 API가 타임아웃(3초)이 발생하더라도 DB 커넥션 풀에는 영향을 주지 않음
try {
paymentClient.pay(orderAmount)
} catch (e: Exception) {
log.error("Payment failed", e)
throw RuntimeException("결제 처리 중 오류 발생")
}
// 3. 상태 변경을 위한 짧은 트랜잭션 실행 (커넥션 잠깐 점유 후 즉시 반납)
orderCommandService.completeOrder(orderId)
}
}
@Service
class OrderQueryService(private val orderRepository: OrderRepository) {
@Transactional(readOnly = true)
fun getOrderAmount(orderId: Long): Long {
return orderRepository.findById(orderId).amount
}
}
@Service
class OrderCommandService(private val orderRepository: OrderRepository) {
@Transactional
fun completeOrder(orderId: Long) {
orderRepository.findById(orderId).complete()
}
}
위의 해결방안과 같이 네트워크 io 작업 외부 api 호출등은 Transactional 밖으로 빼내는 아키텍처 리팩토링을 적용해 외부 시스템의 장애가 내부 db 커넥션 풀 고갈로 전파되는 현상을 구조적으로 차단할 수 있는데.
해당 api가 꼭 같은 트랜잭션에 묶여있어야하는가. 원자적으로 동작해야하는가. 얼마나 기능적으로 둘이 결합이 잘되어야하는가를 판단해서 적용해보면 좋을듯 하다.