DB 커넥션 풀, 왜 필요하고 어떻게 설정해야 할까
목차
스레드 풀에 대해 공부하고 개념을 재정립 했는데, 자연스럽게 또 다른 의문이 생겼어요. “그럼 DB 커넥션은 어떻게 관리되는 거지?” 스레드 풀이 200개로 제한되어 있다면, DB 커넥션도 제한이 있을 것 같았거든요. 그래서 DB 커넥션 풀에 대해 파헤쳐 보기로 했어요.
1. 왜 커넥션 풀이 필요할까?
1.1 JDBC의 등장과 문제점
1997년, Java에 JDBC(Java Database Connectivity)가 등장했어요. 드디어 자바에서 데이터베이스를 다룰 수 있게 된 거예요. 하지만 JDBC에는 치명적인 문제가 있었어요.
// 1997년 스타일 JDBC 코드class OldSchoolDatabase { void insertUser(String name) throws SQLException { // 1. DB 연결 생성 (비싸다!) Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/mydb", "username", "password" );
// 2. 쿼리 실행 Statement stmt = conn.createStatement(); stmt.executeUpdate("INSERT INTO users VALUES ('" + name + "')");
// 3. 연결 종료 stmt.close(); conn.close(); // 다시 만들어야 함... }}매번 커넥션을 생성하고 제거하는 비용이 엄청나게 비쌌어요. TCP 연결 수립(3-way handshake), DB 인증, 메모리 할당 등 수많은 작업이 필요했거든요.
출처: Baeldung - A Simple Guide to Connection Pooling in Java, Progress - JDBC Connection Pooling in Java Tutorial
1.2 커넥션 한 번 만드는 데 얼마나 걸릴까?
실제로 DB 커넥션을 생성하는 과정은 다음과 같아요:
- TCP 소켓 연결: 네트워크를 통해 DB 서버와 3-way handshake
- DB 인증: 사용자 이름/비밀번호 검증
- 세션 생성: DB 서버 내부에 세션 객체 할당
- 메타데이터 로딩: 데이터베이스 설정, 인코딩 정보 등
이 과정은 수십 밀리초에서 수백 밀리초가 걸려요. 로컬 네트워크에서도 20~50ms 정도 소요돼요. 만약 초당 1000개의 요청이 들어온다면? 그냥 커넥션 만드는 데만 20초가 걸린다는 뜻이에요.
// 커넥션 생성 비용 측정class ConnectionBenchmark { public static void main(String[] args) { long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) { try (Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "root", "password")) { // 커넥션만 만들고 바로 닫음 } }
long end = System.currentTimeMillis(); System.out.println("100개 커넥션 생성 시간: " + (end - start) + "ms"); // 결과: 약 3000~5000ms (30~50ms per connection) }}1.3 커넥션 풀의 탄생
이 문제를 해결하기 위해 커넥션 풀(Connection Pool) 개념이 등장했어요.
// 커넥션 풀의 개념class SimpleConnectionPool { private Queue<Connection> pool = new LinkedList<>();
// 애플리케이션 시작 시 미리 생성 public SimpleConnectionPool(int poolSize) { for (int i = 0; i < poolSize; i++) { pool.add(createNewConnection()); } }
// 커넥션 빌려주기 public Connection getConnection() { if (pool.isEmpty()) { throw new SQLException("커넥션 풀이 비었습니다!"); } return pool.poll(); }
// 커넥션 반납 public void returnConnection(Connection conn) { pool.offer(conn); }}핵심 아이디어: 커넥션을 미리 만들어 두고 재사용하자!
1.4 왜 커넥션 풀은 싱글톤으로 만들까?
커넥션 풀 자체를 싱글톤 패턴으로 구현하는 이유는 커넥션 풀을 만드는 것도 비용이 엄청나게 비싸기 때문이에요.
커넥션 풀 생성 비용
// 커넥션 풀을 매번 새로 만든다면?class BadConnectionPoolUsage { void processRequest() throws SQLException { // 문제: 요청마다 풀을 새로 생성! HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); config.setUsername("user"); config.setPassword("password"); config.setMaximumPoolSize(10);
// 풀 생성 시 10개 커넥션을 미리 만듦 HikariDataSource pool = new HikariDataSource(config); // 비싸다!
// 사용 Connection conn = pool.getConnection(); // ... 쿼리 실행 ... conn.close();
pool.close(); // 풀을 닫으면 10개 커넥션 모두 종료! }}문제점:
- 요청마다 10개의 커넥션을 새로 생성 (각 30
50ms) → **300500ms 소요** - 요청이 끝날 때마다 10개의 커넥션을 모두 종료
- 커넥션 풀의 의미가 완전히 사라짐!
싱글톤 패턴으로 해결
// 올바른 방법: 싱글톤으로 하나만 만들기class ConnectionPoolManager { // 애플리케이션 전체에서 딱 1개만 존재! private static HikariDataSource dataSource;
// 애플리케이션 시작 시 한 번만 초기화 static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); config.setUsername("user"); config.setPassword("password"); config.setMaximumPoolSize(10);
dataSource = new HikariDataSource(config); // 여기서 10개 커넥션을 미리 생성 (시작 시 한 번만!) }
public static DataSource getDataSource() { return dataSource; }}// 사용하는 곳@Serviceclass UserService { @Autowired private DataSource dataSource; // 싱글톤 풀 주입
void processRequest() throws SQLException { // 풀에서 커넥션만 빌려옴 (빠름!) Connection conn = dataSource.getConnection(); // ... 쿼리 실행 ... conn.close(); // 풀에 반납 (종료 아님!) }}Spring의 DataSource 빈
Spring에서는 자동으로 싱글톤으로 관리해 줘요:
spring: datasource: hikari: jdbc-url: jdbc:mysql://localhost:3306/mydb username: user password: password maximum-pool-size: 10@Configurationpublic class DataSourceConfig {
@Bean // Spring이 싱글톤으로 관리! public DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl(jdbcUrl); config.setUsername(username); config.setPassword(password); config.setMaximumPoolSize(10);
// 애플리케이션 시작 시 딱 1번만 실행됨! return new HikariDataSource(config); }}비용 비교
잘못된 방법 (매번 풀 생성):

올바른 방법 (싱글톤 풀):

메모리 관점
// 잘못된 방법: 메모리 낭비class MemoryWaste { void badApproach() { // 요청 1: 커넥션 풀 1개 (10개 커넥션) = 20MB HikariDataSource pool1 = new HikariDataSource(config);
// 요청 2: 커넥션 풀 1개 (10개 커넥션) = 20MB HikariDataSource pool2 = new HikariDataSource(config);
// 요청 3: 커넥션 풀 1개 (10개 커넥션) = 20MB HikariDataSource pool3 = new HikariDataSource(config);
// 총 메모리: 60MB + GC 부담 증가 }}
// 올바른 방법: 싱글톤class MemoryEfficient { private static HikariDataSource pool = new HikariDataSource(config); // 애플리케이션 전체에서 20MB만 사용
void goodApproach() { // 모든 요청이 같은 풀 공유 Connection conn = pool.getConnection(); }}핵심 정리
커넥션 풀을 싱글톤으로 만드는 이유:
- 초기화 비용 절감: 풀 생성 시 모든 커넥션을 미리 만드는데, 이 과정을 한 번만 하면 돼요
- 메모리 효율성: 하나의 풀만 유지하므로 메모리 사용량이 최소화돼요
- 커넥션 재사용 극대화: 애플리케이션 전체에서 같은 커넥션들을 공유해서 사용해요
- 리소스 관리 단순화: 풀이 하나만 있으면 모니터링과 관리가 쉬워요
싱글톤이 아니었다면?
- 커넥션 풀의 의미가 없어진다 (매번 새로 만들면 일반 커넥션과 다를 게 없음)
- 메모리 폭발과 GC 부담 증가
- DB 서버에 불필요한 커넥션이 과도하게 생성됨
출처: Baeldung - A Simple Guide to Connection Pooling in Java, Singleton Pattern in Connection Pool
2. 커넥션 풀의 역사
2.1 초기 구현체들 (2000년대 초반)
JDBC 2.0에서 커넥션 풀링을 위한 표준 API가 추가되었고, JDBC 3.0에서는 핵심 API에 포함되었어요. 하지만 JDBC는 인터페이스만 제공했고, 실제 구현은 각 라이브러리가 담당했어요.
출처: Progress - JDBC Connection Pooling in Java Tutorial, PostgreSQL JDBC - Connection Pools and Data Sources
C3P0 (2001년 경)
가장 오래되고 유명한 커넥션 풀 라이브러리였어요. 하이버네이트와 함께 많이 사용되었고요.
<!-- C3P0 설정 (2000년대 스타일) --><c3p0-config> <default-config> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost/test</property> <property name="user">root</property> <property name="password">password</property>
<property name="minPoolSize">5</property> <property name="maxPoolSize">20</property> <property name="acquireIncrement">5</property> </default-config></c3p0-config>문제점:
- 설정이 너무 복잡했어요
- 잘못 설정하면 성능 이슈나 데드락이 발생했어요
- 멀티 코어 CPU를 제대로 활용하지 못했어요 (단일 락 사용)
- 프로젝트가 사실상 중단됨 (2017년 기준 기여자 2명, 커밋 2개)
Apache Commons DBCP (2001년)
아파치 재단에서 만든 커넥션 풀이었어요. 많은 프로젝트에서 사용되었고요.
문제점:
- 단일 스레드와 락을 사용해 전체 풀을 잠가 스레드 안전성을 보장했어요
- 느렸고 멀티 코어 CPU를 제대로 활용하지 못했어요
- 벤치마크 결과 HikariCP 대비 2000배 이상 느렸다 (5 ops/ms vs 45,289 ops/ms)
Tomcat JDBC Pool (2010년)
톰캣 팀이 DBCP의 문제를 개선하기 위해 만든 풀이었어요.
// Tomcat JDBC Pool 설정org.apache.tomcat.jdbc.pool.DataSource ds = new org.apache.tomcat.jdbc.pool.DataSource();ds.setUrl("jdbc:mysql://localhost:3306/test");ds.setDriverClassName("com.mysql.jdbc.Driver");ds.setUsername("root");ds.setPassword("password");ds.setMaxActive(100);ds.setMaxIdle(10);DBCP보다 성능이 훨씬 좋았지만, 여전히 HikariCP에는 미치지 못했다 (2,329 ops/ms).
출처: Stack Overflow - Connection pooling options with JDBC: DBCP vs C3P0, Baeldung - Using c3p0 with Hibernate
2.2 HikariCP의 등장 (2012년)
2012년, Brett Wooldridge는 회사 프로토타입을 만들면서 커넥션 풀이 필요했어요. 기존 풀들을 사용해 봤지만 로드 테스트 중 데드락과 예외가 계속 발생했거든요.
오픈소스니까 코드를 받아서 고쳐보려고 했는데, 예상보다 수천 줄이나 더 많은 코드를 발견했어요. 결국 좌절과 필요성 때문에 직접 만들기로 결심했어요. 그렇게 HikariCP가 탄생했죠.
HikariCP의 설계 철학:
- “Fast, simple, reliable”
- 불필요한 기능을 의도적으로 제외 (미니멀리즘)
- 바이트코드 수준의 최적화
- 제로 오버헤드를 추구
- 약 165KB의 초경량 라이브러리
성능 차이:
| 풀 이름 | 성능 (ops/ms) | HikariCP 대비 |
|---|---|---|
| HikariCP | 45,289 | 1x (기준) |
| Tomcat JDBC | 2,329 | 19배 느림 |
| DBCP2 | 21.75 | 2,081배 느림 |
출처: jOOQ Blog - Brett Wooldridge Shows What it Takes to Write the Fastest Java Connection Pool, HikariCP Benchmark
2.3 Spring Boot의 선택 (2018년)
2018년, Spring Boot 2.0부터 HikariCP를 기본 커넥션 풀로 채택했어요. 이전에는 Tomcat JDBC Pool을 사용했었고요.
# Spring Boot 2.0+ 기본 설정spring: datasource: hikari: # HikariCP가 기본이 됨 maximum-pool-size: 10 connection-timeout: 300003. HikariCP는 왜 빠를까?
3.1 바이트코드 수준의 최적화
Brett Wooldridge는 자바 바이트코드 수준까지 내려가서 최적화했어요. 몇 가지 예시를 볼게요.
불필요한 메서드 호출 제거
// 다른 풀들: ArrayList.get() 사용Connection conn = connectionList.get(index);// ArrayList.get()은 범위 체크를 함public E get(int index) { rangeCheck(index); // 불필요한 체크! return elementData[index];}
// HikariCP: FastList 직접 구현Connection conn = fastList.get(index);// 범위 체크 없이 직접 접근public T get(int index) { return elementData[index]; // 바로 접근}ConcurrentBag: 락 경합 최소화
// HikariCP의 ConcurrentBagclass ConcurrentBag<T> { // 각 스레드가 자기 전용 리스트를 가짐 (락 없음!) private ThreadLocal<List<T>> threadList = new ThreadLocal<>();
// 공유 큐 (백업용) private CopyOnWriteArrayList<T> sharedList;
T borrow() { // 1단계: 내 전용 리스트에서 찾기 (락 없음!) List<T> list = threadList.get(); for (T item : list) { if (item.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return item; } }
// 2단계: 공유 리스트에서 찾기 for (T item : sharedList) { if (item.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return item; } }
return null; }}핵심: 스레드별 전용 리스트를 사용해 락 경합을 최소화했어요.
출처: HikariCP GitHub - ConcurrentBag 구현, jOOQ Blog - Brett Wooldridge Shows What it Takes to Write the Fastest Java Connection Pool
3.2 불필요한 기능 제거
PreparedStatement 캐싱을 하지 않는 이유
대부분의 JDBC 드라이버(PostgreSQL, Oracle, MySQL 등)는 이미 드라이버 수준에서 캐싱을 해요.
// MySQL Connector/J는 이미 캐싱을 함Connection conn = dataSource.getConnection();// 드라이버 내부에서 이미 캐싱됨!PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");커넥션 풀 레벨에서 또 캐싱하면 중복이고 오버헤드만 늘어나요. HikariCP는 이를 과감히 제거했어요.
출처: HikariCP GitHub - About Pool Sizing, MySQL Connector/J Connection Pooling
4. HikariCP 설정 완벽 가이드
4.1 기본 설정값
| 설정 | 기본값 | 설명 |
|---|---|---|
maximumPoolSize | 10 | 최대 커넥션 수 |
minimumIdle | maximumPoolSize와 동일 | 최소 유휴 커넥션 수 |
connectionTimeout | 30초 | 커넥션 획득 대기 시간 |
idleTimeout | 10분 | 유휴 커넥션 제거 시간 |
maxLifetime | 30분 | 커넥션 최대 생존 시간 |
keepaliveTime | 2분 | 커넥션 유지 확인 간격 |
validationTimeout | 5초 | 연결 유효성 검사 타임아웃 |
4.2 Spring Boot 설정 예시
spring: datasource: hikari: # 커넥션 풀 크기 maximum-pool-size: 20 minimum-idle: 20
# 타임아웃 connection-timeout: 3000 # 3초 validation-timeout: 1000 # 1초
# 생명주기 max-lifetime: 580000 # 9분 40초 (DB wait_timeout보다 짧게) idle-timeout: 600000 # 10분 keepalive-time: 30000 # 30초
# 기타 auto-commit: true leak-detection-threshold: 60000 # 60초4.3 중요한 설정들
4.3.1 maximumPoolSize와 minimumIdle
권장: 같은 값으로 설정하라!
hikari: maximum-pool-size: 20 minimum-idle: 20 # 같은 값!이유:
- 유휴 커넥션 제한 기능이 동작하지 않도록 하기 위함
- 게임 서버나 라이브 스트리밍 서버처럼 항상 활성 상태인 시스템에서는 굳이 커넥션을 줄일 필요가 없다
- 커넥션 증가/감소 과정에서 발생하는 오버헤드를 제거
4.3.2 connectionTimeout
권장: 0.5~3초
hikari: connection-timeout: 3000 # 3초이유:
- 기본값 30초는 너무 길다
- 응답이 없는 것보다는 빨리 에러를 반환하는 게 낫다
- 사용자는 30초를 기다리지 않는다
4.3.3 maxLifetime
권장: DB의 wait_timeout보다 2~3초 짧게
hikari: max-lifetime: 580000 # 9분 40초이유:
- MySQL의 기본
wait_timeout은 8시간(28800초) - 하지만 실제 운영에서는 10분(600초) 정도로 설정하는 경우가 많다
- HikariCP의
max-lifetime을 DB보다 짧게 설정하면 DB가 먼저 끊는 것을 방지
-- MySQL에서 wait_timeout 확인SHOW VARIABLES LIKE 'wait_timeout';
-- wait_timeout 설정 (10분)SET GLOBAL wait_timeout = 600;SET GLOBAL interactive_timeout = 600;4.3.4 leakDetectionThreshold
권장: 60초
hikari: leak-detection-threshold: 60000 # 60초이유:
- 너무 짧게 설정하면 정상 트랜잭션도 누수로 오판
- 너무 길게 설정하면 실제 누수를 감지하지 못함
- 60초 정도면 대부분의 정상 트랜잭션은 완료됨
// 누수 발생 예시@Serviceclass UserService { @Autowired private DataSource dataSource;
void badMethod() throws SQLException { Connection conn = dataSource.getConnection(); // 쿼리 실행 Statement stmt = conn.createStatement(); stmt.executeQuery("SELECT * FROM users");
// 문제: conn.close()를 안 했다! // 60초 후 HikariCP가 경고 로그 출력 // WARN - Connection leak detection triggered }}출처: HikariCP Configuration - HikariCP 설정 가이드, Spring Boot HikariCP
5. 커넥션 풀 크기는 어떻게 정할까?
5.1 유명한 공식
HikariCP 위키에 나오는 공식이 있어요:
connections = (core_count × 2) + effective_spindle_countcore_count: CPU 코어 수effective_spindle_count: DB 서버가 동시에 처리할 수 있는 디스크 I/O 수
예시:
- 8코어 CPU, HDD 1개 사용
- connections = (8 × 2) + 1 = 17개
5.2 왜 “core_count × 2”일까?
CPU와 디스크/네트워크의 속도 차이 때문이에요.
CPU: 1 GHz = 10억 사이클/초디스크: 100 IOPS = 100번/초네트워크: 1ms 레이턴시 = 1000번/초
속도 차이: 약 100만 배!CPU가 디스크나 네트워크를 기다리는 동안 다른 스레드를 처리할 수 있어요. 그래서 코어 수보다 많은 커넥션이 필요해요.
// 커넥션이 작업하는 시간 분석class ConnectionWorkload { void processRequest() { businessLogic();
// 2. DB 쿼리: 10ms (I/O 대기) executeQuery();
// 3. CPU 작업: 0.1ms processResult();
// 총 시간: 10.2ms // 실제 CPU 사용: 0.2ms (약 2%) // I/O 대기: 10ms (약 98%) }}CPU가 98% 시간을 놀고 있어요! 그래서 CPU 코어당 2개 이상의 커넥션이 효율적이에요.
5.3 실제로는 어떻게 정할까?
공식은 참고만 하고, 실제 측정이 중요하다!
단계 1: 스레드 수 확인
server: tomcat: threads: max: 200 # 톰캣 최대 스레드단계 2: 작업당 필요 커넥션 수 확인
@Serviceclass OrderService { @Transactional void createOrder(Order order) { // 1개의 커넥션만 사용 orderRepository.save(order); itemRepository.updateStock(order.getItemId()); }}
@Serviceclass ComplexService { void complexOperation() { // 문제: 2개의 커넥션이 필요! Connection conn1 = dataSource.getConnection(); // 1번 // ... 작업 중 ... Connection conn2 = dataSource.getConnection(); // 2번 // 데드락 위험! }}단계 3: 공식 적용
pool_size = thread_count × (connections_per_task - 1) + 1예시:
- 톰캣 스레드: 200개
- 작업당 커넥션: 1개
- pool_size = 200 × (1 - 1) + 1 = 1개?!
이건 말이 안 돼요. 왜일까요?
단계 4: 실제 동시성 고려
톰캣 스레드가 200개라고 해서 동시에 200개가 모두 DB를 쓰는 건 아니에요.
@RestControllerclass StreamingController { @GetMapping("/api/stream/{id}") void getStream(@PathVariable Long id) { // 1. DB 조회 (커넥션 사용) Stream stream = streamRepository.findById(id);
// 2. 비즈니스 로직 (커넥션 사용 안 함) processStream(stream);
// 3. 캐시 확인 (커넥션 사용 안 함) cacheService.get(id);
// 4. 외부 API 호출 (커넥션 사용 안 함) notificationService.send(stream); }}실제로는 10~20% 정도만 동시에 DB를 사용한다고 가정하면:
- 200 × 0.2 = 40개
5.4 넷마블의 게임 서버 설정
넷마블 기술 블로그에 따르면, 게임 서버에서는 다음과 같이 설정했어요:
hikari: maximum-pool-size: 50 # CPU 코어 수 기준, 성능 테스트로 결정 minimum-idle: 50 # maximum과 동일중요한 점:
- 초기값은 CPU 코어 수로 시작
- 성능 테스트를 통해 조정
- 모니터링하면서 최적값 찾기
출처: HikariCP GitHub - About Pool Sizing, 넷마블 - 게임 서버 시스템을 위한 HikariCP 옵션 및 권장 설정, Carrey’s 기술블로그 - HikariCP Maximum Pool Size 설정 시 고려해야할 부분
6. 언제 서버가 터질까?
6.1 시나리오 1: 커넥션 풀 고갈
상황: 갑자기 트래픽이 몰렸다.
// 설정hikari: maximum-pool-size: 10 connection-timeout: 3000 // 3초
// 상황// 1초에 100개 요청 들어옴// 각 요청은 DB 쿼리에 5초 소요
// 1초 후: 100개 요청이 10개 커넥션을 대기// 90개 요청이 큐에서 대기// 3초 후: connection-timeout 발생!
// 로그org.springframework.dao.DataAccessResourceFailureException: Unable to acquire JDBC ConnectionCaused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3000ms.해결:
- 커넥션 풀 크기 증가
hikari: maximum-pool-size: 50 # 10 → 50- 쿼리 최적화
-- Before: 5초SELECT * FROM streams WHERE status = 'LIVE';
-- After: 0.1초 (인덱스 추가)CREATE INDEX idx_stream_status ON streams(status);SELECT * FROM streams WHERE status = 'LIVE';- 캐싱 도입
@Serviceclass StreamService { @Cacheable("liveStreams") List<Stream> getLiveStreams() { // DB 조회 횟수 감소 return streamRepository.findByStatus(StreamStatus.LIVE); }}6.2 시나리오 2: 커넥션 누수
상황: 커넥션을 반환하지 않았다.
@Serviceclass BadService { @Autowired private DataSource dataSource;
void leakyMethod() throws SQLException { Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// 처리...
// 문제: close()를 안 했다! // conn, stmt, rs가 모두 닫히지 않음 }}
// 이 메서드를 10번 호출하면?// 커넥션 풀(10개)이 모두 고갈됨!증상:
WARN - Connection leak detection triggered for connection해결:
- try-with-resources 사용 (Java 7+)
@Serviceclass GoodService { @Autowired private DataSource dataSource;
void goodMethod() throws SQLException { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
// 처리...
} // 자동으로 close() 호출! }}- JPA/MyBatis 사용
@Serviceclass BetterService { @Autowired private UserRepository userRepository;
void betterMethod() { // JPA가 알아서 커넥션 관리 List<User> users = userRepository.findAll(); }}- 누수 감지 설정
hikari: leak-detection-threshold: 60000 # 60초6.3 시나리오 3: DB 서버가 커넥션을 끊어버림
상황: MySQL의 wait_timeout이 만료되었다.
-- MySQL 기본 설정SHOW VARIABLES LIKE 'wait_timeout';-- wait_timeout = 28800 (8시간)
-- 하지만 실제 운영에서는 짧게 설정하는 경우가 많음SET GLOBAL wait_timeout = 60; -- 60초로 변경// 1. 커넥션 획득Connection conn = dataSource.getConnection();
// 2. 아무것도 안 하고 70초 대기Thread.sleep(70000);
// 3. 쿼리 실행 시도stmt.executeQuery("SELECT * FROM users");// 에러!// Communications link failure// The last packet successfully received from the server// was 70,000 milliseconds ago해결:
- HikariCP maxLifetime 설정
hikari: max-lifetime: 50000 # 50초 (wait_timeout보다 짧게)- DB wait_timeout 증가
SET GLOBAL wait_timeout = 600; -- 10분SET GLOBAL interactive_timeout = 600; -- 10분- keepaliveTime 설정 (HikariCP 4.0+)
hikari: keepalive-time: 30000 # 30초마다 연결 확인keepaliveTime의 원리:
// HikariCP 내부 동작class HikariPool { void keepalive() { for (Connection conn : idleConnections) { if (idleTime > keepaliveTime) { // 간단한 쿼리로 연결 확인 conn.isValid(validationTimeout); // 또는 stmt.execute("SELECT 1"); } } }}출처: 후덥의 기술블로그 - HikariCP는 test-while-idle과 같은 커넥션 갱신 기능이 없을까?, SK C&C - MySql/MariaDB에서 발생하는 Connection 끊김 문제 해결하기
6.4 시나리오 4: 데드락
상황: 커넥션 풀이 부족해서 데드락 발생.
@Serviceclass DeadlockService { @Transactional void problemMethod() { // 첫 번째 커넥션 획득 Connection conn1 = dataSource.getConnection();
// ... 작업 중 ...
// 두 번째 커넥션 시도 // 하지만 풀에 남은 커넥션이 없음! Connection conn2 = dataSource.getConnection(); // 영원히 대기... (데드락!) }}발생 조건:

해결:
- 풀 크기 공식 적용
# pool_size = thread_count × (connections_per_task - 1) + 1# 16 × (2 - 1) + 1 = 17
hikari: maximum-pool-size: 17- 작업당 커넥션 수 줄이기
@Serviceclass FixedService { @Transactional void fixedMethod() { // 하나의 트랜잭션으로 통합 // 1개의 커넥션만 사용 orderRepository.save(order); itemRepository.updateStock(itemId); }}6.5 시나리오 5: 메모리 부족
상황: 커넥션이 너무 많아서 메모리 부족.
// 커넥션 하나당 메모리 사용량class ConnectionMemory { // TCP 소켓: ~64KB // ResultSet 버퍼: ~1MB // Statement 객체들: ~100KB // 메타데이터: ~100KB
// 총합: 약 1~2MB per connection}
// 커넥션 1000개 = 1~2GB 메모리!hikari: maximum-pool-size: 1000 // 위험!해결:
- 적절한 풀 크기 유지
hikari: maximum-pool-size: 50 # 적정 수준- DB 서버 리소스 확인
-- MySQL 최대 커넥션 확인SHOW VARIABLES LIKE 'max_connections';-- max_connections = 151 (기본값)
-- 현재 커넥션 확인SHOW STATUS LIKE 'Threads_connected';- 애플리케이션 서버 분산
# 서버 1대: 커넥션 50개# 서버 3대: 커넥션 150개# DB max_connections: 200
# 여유 있게 설정7. 실제 장애 사례
7.1 카카오페이: R2DBC 커넥션 풀 미생성
문제: Spring WebFlux + R2DBC에서 jar 파일 실행 시 커넥션 풀이 초기화되지 않음.
// r2dbc-pool의 특성// 생성 시점에 커넥션을 만들지 않고,// 실제 필요할 때 만듦 (Lazy Initialization)
// IntelliJ에서는 동작함 (왜?)// → Actuator의 HealthCheck가 JMX를 통해 자동 호출// → 이 과정에서 커넥션 풀이 초기화됨
// jar 실행 시에는 동작 안 함// → HealthCheck가 자동 호출되지 않음// → 커넥션 풀이 초기화 안 됨// → 첫 요청에서 지연 발생!해결:
// 명시적으로 warmup 호출r2dbcPool.warmup().block()교훈:
- 개발 환경과 운영 환경의 차이를 검증하세요
- IDE의 자동화 기능이 실제 동작을 왜곡할 수 있어요
- 새로운 기술 도입 시 기대 동작과 실제 동작을 확인하세요
7.2 SK C&C: MySQL wait_timeout 문제
문제: wait_timeout을 60초로 짧게 설정해서 커넥션이 계속 끊김.
-- 잘못된 설정SET GLOBAL wait_timeout = 60; -- 너무 짧음!// 60초마다 커넥션이 끊김// HikariCP가 끊긴 커넥션을 감지하지 못함// 애플리케이션에서 에러 발생!
// 에러 로그Communications link failureThe last packet successfully received from the serverwas 70,000 milliseconds ago해결:
# HikariCP 설정hikari: max-lifetime: 50000 # 50초 (wait_timeout보다 짧게)
# MySQL 설정wait_timeout: 180 # 3분교훈:
- DB 타임아웃 설정은 신중하게 해야 해요
- HikariCP의
max-lifetime은 DBwait_timeout보다 짧게 설정하세요 - 너무 짧은 타임아웃은 성능 저하를 유발해요
7.3 개인 프로젝트: 커넥션 누수
문제: leak-detection-threshold를 2초로 설정해서 오탐 발생.
# 잘못된 설정hikari: leak-detection-threshold: 2000 # 2초 (너무 짧음!)@Serviceclass SchedulerService { @Scheduled(fixedDelay = 1000) void scheduledTask() { // 정상적인 작업인데 3초 소요 userRepository.findAll(); // 3초
// HikariCP가 누수로 오판! // WARN - Apparent connection leak detected }}해결:
hikari: leak-detection-threshold: 60000 # 60초교훈:
- 너무 짧은 누수 감지 시간은 오탐을 유발해요
- 정상 트랜잭션 처리 시간을 고려해야 해요
- 60초 정도가 적절해요
7.4 생산성 저하: MySQL AbandonedConnectionCleanupThread
문제: DB 커넥션이 이상하게 많이 생성되어 메모리 사용량 증가.
// MySQL Connector/J의 문제// AbandonedConnectionCleanupThread가 계속 생성됨// 메모리 누수 발생!
// Thread Dump"MySQL-AB-1" daemon"MySQL-AB-2" daemon"MySQL-AB-3" daemon... (수백 개)해결:
# JVM 옵션 추가-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true교훈:
- 일반 서비스는 커넥션을 직접 관리하지 않아요
- JDBC 드라이버의 자동 정리 기능이 오히려 해가 될 수 있어요
- 불필요한 기능은 비활성화하세요
출처: 카카오페이 - R2DBC Connection Pool 실종 사건, SK C&C - MySql/MariaDB에서 발생하는 Connection 끊김 문제 해결하기, velog - DB 커넥션 풀 누수 문제
8. 모니터링과 최적화
8.1 HikariCP 메트릭
HikariCP는 다양한 메트릭을 제공해요.
# Actuator 설정management: endpoints: web: exposure: include: metrics,health metrics: enable: hikaricp: true// 확인 가능한 메트릭hikaricp.connections.active // 활성 커넥션 수hikaricp.connections.idle // 유휴 커넥션 수hikaricp.connections.pending // 대기 중인 요청 수hikaricp.connections.timeout // 타임아웃 발생 횟수hikaricp.connections.usage // 커넥션 사용 시간8.2 Prometheus + Grafana
scrape_configs: - job_name: 'spring-app' metrics_path: '/actuator/prometheus' static_configs: - targets: ['localhost:8080']알아야 할 지표:
- active / maximum: 80% 넘으면 풀 크기 증가 고려
- pending: 0이 아니면 풀 부족
- timeout: 계속 발생하면 쿼리 최적화 또는 풀 증가 필요
- usage: 평균 사용 시간이 길면 쿼리 최적화 필요
8.3 최적화 체크리스트
1단계: 설정 확인
-
maximumPoolSize=minimumIdle로 설정했는가? -
connectionTimeout을 3초 이하로 설정했는가? -
maxLifetime< DBwait_timeout인가? -
leakDetectionThreshold를 60초로 설정했는가?
2단계: 코드 확인
- try-with-resources를 사용하는가?
- JPA/MyBatis를 사용하는가? (직접 JDBC 사용 지양)
- 한 트랜잭션에서 여러 커넥션을 사용하지 않는가?
3단계: 쿼리 최적화
- 슬로우 쿼리를 찾았는가?
- 인덱스를 적절히 사용하는가?
- N+1 쿼리 문제를 해결했는가?
4단계: 모니터링
- HikariCP 메트릭을 수집하는가?
- 알람을 설정했는가? (active > 80%, timeout > 0)
- DB 서버 모니터링도 하는가?
9. 정리
9.1 핵심 요약
-
커넥션 생성은 비싸다 (20~50ms)
- 커넥션 풀로 재사용하자
-
HikariCP가 최고다
- Spring Boot 2.0+의 기본 풀
- 다른 풀보다 수천 배 빠르다
-
설정이 중요하다
maximumPoolSize=minimumIdleconnectionTimeout= 3초maxLifetime< DBwait_timeout
-
풀 크기 공식
connections = (core_count × 2) + effective_spindle_count하지만 실제 측정이 더 중요!
-
주요 장애 원인
- 커넥션 풀 고갈
- 커넥션 누수 (close 안 함)
- DB 타임아웃
- 데드락
-
해결 방법
- try-with-resources 사용
- JPA/MyBatis 사용
- 쿼리 최적화
- 모니터링
9.2 실제 설정 예시
# 라이브 스트리밍 서버 설정spring: datasource: hikari: # 커넥션 풀 maximum-pool-size: 50 minimum-idle: 50
# 타임아웃 connection-timeout: 3000 validation-timeout: 1000
# 생명주기 max-lifetime: 580000 # 9분 40초 idle-timeout: 600000 # 10분 keepalive-time: 30000 # 30초
# 누수 감지 leak-detection-threshold: 60000
# MySQL 설정# wait_timeout = 600 (10분)# interactive_timeout = 600 (10분)9.3 마치며
스레드 풀에 이어 커넥션 풀까지 공부하고 나니, 이제 라이브 스트리밍 서버가 어떻게 동시 요청을 처리하는지 전체 그림이 보이기 시작했어요.

다음엔 캐싱과 비동기 처리에 대해 더 공부해서, cs 개념을 재정립하고 더 빠르고 안정적인 서버를 만들도록 노력해야겠어요.
참고 자료
공식 문서
- HikariCP GitHub - HikariCP 공식 저장소
- HikariCP Configuration - HikariCP 설정 가이드
- MySQL Connector/J Connection Pooling - MySQL JDBC 커넥션 풀링
- Apache Tomcat JDBC Pool - Tomcat JDBC Pool 문서
- PostgreSQL JDBC Connection Pools - PostgreSQL JDBC 커넥션 풀
- Spring Boot HikariCP - Spring Boot 공식 문서
기술 블로그
- 넷마블 - 게임 서버 시스템을 위한 HikariCP 옵션 및 권장 설정 - 게임 서버 최적화 사례
- 카카오페이 - R2DBC Connection Pool 실종 사건 - R2DBC 커넥션 풀 장애 사례
- SK C&C - MySql/MariaDB에서 발생하는 Connection 끊김 문제 해결하기 - wait_timeout 문제 해결
- Carrey’s 기술블로그 - HikariCP Maximum Pool Size 설정 시 고려해야할 부분 - 데드락 방지를 위한 풀 크기 계산
- 후덥의 기술블로그 - HikariCP는 test-while-idle과 같은 커넥션 갱신 기능이 없을까? - HikariCP 커넥션 관리 방식
- jOOQ Blog - Brett Wooldridge Shows What it Takes to Write the Fastest Java Connection Pool - HikariCP 개발 스토리
한글 자료
- hudi.blog - 데이터베이스 커넥션 풀 (Connection Pool)과 HikariCP - 커넥션 풀 기본 개념
- velog - DB 커넥션 풀 누수 문제 - 누수 감지 설정 문제
- haon.blog - HikariCP 와 데이터베이스 커넥션 풀(DBCP) 최적화 고민하기 - 커넥션 풀 최적화 이론
- Baeldung - A Simple Guide to Connection Pooling in Java - Java 커넥션 풀링 가이드
- Progress - JDBC Connection Pooling in Java Tutorial - JDBC 커넥션 풀링 튜토리얼
벤치마크
- HikariCP Benchmark - HikariCP 공식 벤치마크
- Medium - Database Connection Pool Analysis - 커넥션 풀 성능 비교
After studying thread pools and re-establishing my understanding of the concept, another question naturally arose: “So how are DB connections managed?” If the thread pool is limited to 200, there must be a limit on DB connections too. So I decided to dig deep into DB connection pools.
1. Why Do We Need Connection Pools?
1.1 The Emergence and Problems of JDBC
In 1997, JDBC (Java Database Connectivity) was introduced to Java. Finally, Java could interact with databases. However, JDBC had a critical problem.
// 1997-style JDBC codeclass OldSchoolDatabase { void insertUser(String name) throws SQLException { // 1. Create DB connection (expensive!) Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/mydb", "username", "password" );
// 2. Execute query Statement stmt = conn.createStatement(); stmt.executeUpdate("INSERT INTO users VALUES ('" + name + "')");
// 3. Close connection stmt.close(); conn.close(); // Have to create it again next time... }}The cost of creating and destroying a connection every time was enormous. It required numerous operations: TCP connection establishment (3-way handshake), DB authentication, memory allocation, and more.
Source: Baeldung - A Simple Guide to Connection Pooling in Java, Progress - JDBC Connection Pooling in Java Tutorial
1.2 How Long Does It Take to Create a Single Connection?
The actual process of creating a DB connection involves:
- TCP socket connection: 3-way handshake with the DB server over the network
- DB authentication: Username/password verification
- Session creation: Allocating a session object inside the DB server
- Metadata loading: Database settings, encoding information, etc.
This process takes tens to hundreds of milliseconds. Even on a local network, it takes about 20-50ms. If 1,000 requests come in per second, just creating connections would take 20 seconds.
// Measuring connection creation costclass ConnectionBenchmark { public static void main(String[] args) { long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) { try (Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "root", "password")) { // Just create and immediately close the connection } }
long end = System.currentTimeMillis(); System.out.println("Time to create 100 connections: " + (end - start) + "ms"); // Result: approximately 3000~5000ms (30~50ms per connection) }}1.3 The Birth of Connection Pools
To solve this problem, the concept of a Connection Pool was introduced.
// Connection pool conceptclass SimpleConnectionPool { private Queue<Connection> pool = new LinkedList<>();
// Pre-create connections at application startup public SimpleConnectionPool(int poolSize) { for (int i = 0; i < poolSize; i++) { pool.add(createNewConnection()); } }
// Lend a connection public Connection getConnection() { if (pool.isEmpty()) { throw new SQLException("Connection pool is empty!"); } return pool.poll(); }
// Return a connection public void returnConnection(Connection conn) { pool.offer(conn); }}Core idea: Pre-create connections and reuse them!
1.4 Why Is the Connection Pool a Singleton?
The connection pool itself is implemented as a singleton because creating a connection pool is also extremely expensive.
Connection Pool Creation Cost
// What if you create a new pool every time?class BadConnectionPoolUsage { void processRequest() throws SQLException { // Problem: creating a new pool per request! HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); config.setUsername("user"); config.setPassword("password"); config.setMaximumPoolSize(10);
// Creating the pool pre-creates 10 connections HikariDataSource pool = new HikariDataSource(config); // Expensive!
// Use Connection conn = pool.getConnection(); // ... execute query ... conn.close();
pool.close(); // Closing the pool terminates all 10 connections! }}Problems:
- Creates 10 new connections per request (30-50ms each) — 300-500ms overhead
- Terminates all 10 connections when the request finishes
- The purpose of the connection pool is completely defeated!
Solving with the Singleton Pattern
// Correct approach: create only one with singletonclass ConnectionPoolManager { // Only 1 exists across the entire application! private static HikariDataSource dataSource;
// Initialize once at application startup static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); config.setUsername("user"); config.setPassword("password"); config.setMaximumPoolSize(10);
dataSource = new HikariDataSource(config); // Pre-creates 10 connections here (only once at startup!) }
public static DataSource getDataSource() { return dataSource; }}// Usage@Serviceclass UserService { @Autowired private DataSource dataSource; // Singleton pool injected
void processRequest() throws SQLException { // Just borrows a connection from the pool (fast!) Connection conn = dataSource.getConnection(); // ... execute query ... conn.close(); // Returns to pool (not destroyed!) }}Spring’s DataSource Bean
Spring automatically manages it as a singleton:
spring: datasource: hikari: jdbc-url: jdbc:mysql://localhost:3306/mydb username: user password: password maximum-pool-size: 10@Configurationpublic class DataSourceConfig {
@Bean // Spring manages as singleton! public DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl(jdbcUrl); config.setUsername(username); config.setPassword(password); config.setMaximumPoolSize(10);
// Executed only once at application startup! return new HikariDataSource(config); }}Cost Comparison
Wrong approach (creating pool every time):

Correct approach (singleton pool):

Memory Perspective
// Wrong approach: memory wasteclass MemoryWaste { void badApproach() { // Request 1: 1 connection pool (10 connections) = 20MB HikariDataSource pool1 = new HikariDataSource(config);
// Request 2: 1 connection pool (10 connections) = 20MB HikariDataSource pool2 = new HikariDataSource(config);
// Request 3: 1 connection pool (10 connections) = 20MB HikariDataSource pool3 = new HikariDataSource(config);
// Total memory: 60MB + increased GC pressure }}
// Correct approach: singletonclass MemoryEfficient { private static HikariDataSource pool = new HikariDataSource(config); // Only 20MB used across the entire application
void goodApproach() { // All requests share the same pool Connection conn = pool.getConnection(); }}Key Summary
Why the connection pool should be a singleton:
- Reduced initialization cost: All connections are pre-created when the pool is initialized, and this only needs to happen once
- Memory efficiency: Only one pool is maintained, minimizing memory usage
- Maximized connection reuse: The same connections are shared across the entire application
- Simplified resource management: With only one pool, monitoring and management are easier
What if it weren’t a singleton?
- The purpose of a connection pool would be lost (creating a new one each time is no different from regular connections)
- Memory explosion and increased GC pressure
- Excessive unnecessary connections created on the DB server
Source: Baeldung - A Simple Guide to Connection Pooling in Java, Singleton Pattern in Connection Pool
2. History of Connection Pools
2.1 Early Implementations (Early 2000s)
JDBC 2.0 added a standard API for connection pooling, and JDBC 3.0 included it as a core API. However, JDBC only provided interfaces, and actual implementations were left to individual libraries.
Source: Progress - JDBC Connection Pooling in Java Tutorial, PostgreSQL JDBC - Connection Pools and Data Sources
C3P0 (circa 2001)
The oldest and most well-known connection pool library. It was widely used with Hibernate.
<!-- C3P0 configuration (2000s style) --><c3p0-config> <default-config> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost/test</property> <property name="user">root</property> <property name="password">password</property>
<property name="minPoolSize">5</property> <property name="maxPoolSize">20</property> <property name="acquireIncrement">5</property> </default-config></c3p0-config>Problems:
- Configuration was overly complex
- Misconfiguration could lead to performance issues or deadlocks
- Failed to properly utilize multi-core CPUs (used a single lock)
- The project was effectively abandoned (as of 2017: 2 contributors, 2 commits)
Apache Commons DBCP (2001)
A connection pool created by the Apache Foundation. It was used in many projects.
Problems:
- Used a single thread and lock to lock the entire pool for thread safety
- Slow and failed to utilize multi-core CPUs effectively
- Benchmarks showed it was over 2,000x slower than HikariCP (5 ops/ms vs 45,289 ops/ms)
Tomcat JDBC Pool (2010)
A pool created by the Tomcat team to address DBCP’s shortcomings.
// Tomcat JDBC Pool configurationorg.apache.tomcat.jdbc.pool.DataSource ds = new org.apache.tomcat.jdbc.pool.DataSource();ds.setUrl("jdbc:mysql://localhost:3306/test");ds.setDriverClassName("com.mysql.jdbc.Driver");ds.setUsername("root");ds.setPassword("password");ds.setMaxActive(100);ds.setMaxIdle(10);Performance was much better than DBCP, but still couldn’t match HikariCP (2,329 ops/ms).
Source: Stack Overflow - Connection pooling options with JDBC: DBCP vs C3P0, Baeldung - Using c3p0 with Hibernate
2.2 The Emergence of HikariCP (2012)
In 2012, Brett Wooldridge needed a connection pool while building a company prototype. He tried existing pools but kept encountering deadlocks and exceptions during load testing.
Since they were open source, he pulled the code to try to fix them, but found thousands more lines of code than expected. Out of frustration and necessity, he decided to build his own. That’s how HikariCP was born.
HikariCP’s design philosophy:
- “Fast, simple, reliable”
- Intentionally excluded unnecessary features (minimalism)
- Bytecode-level optimization
- Pursuit of zero overhead
- Ultra-lightweight library at about 165KB
Performance comparison:
| Pool Name | Performance (ops/ms) | vs HikariCP |
|---|---|---|
| HikariCP | 45,289 | 1x (baseline) |
| Tomcat JDBC | 2,329 | 19x slower |
| DBCP2 | 21.75 | 2,081x slower |
Source: jOOQ Blog - Brett Wooldridge Shows What it Takes to Write the Fastest Java Connection Pool, HikariCP Benchmark
2.3 Spring Boot’s Choice (2018)
In 2018, starting with Spring Boot 2.0, HikariCP was adopted as the default connection pool. Previously, Tomcat JDBC Pool had been used.
# Spring Boot 2.0+ default configurationspring: datasource: hikari: # HikariCP is now the default maximum-pool-size: 10 connection-timeout: 300003. Why Is HikariCP So Fast?
3.1 Bytecode-Level Optimization
Brett Wooldridge optimized down to the Java bytecode level. Here are a few examples.
Eliminating Unnecessary Method Calls
// Other pools: use ArrayList.get()Connection conn = connectionList.get(index);// ArrayList.get() performs bounds checkingpublic E get(int index) { rangeCheck(index); // Unnecessary check! return elementData[index];}
// HikariCP: custom FastList implementationConnection conn = fastList.get(index);// Direct access without bounds checkingpublic T get(int index) { return elementData[index]; // Direct access}ConcurrentBag: Minimizing Lock Contention
// HikariCP's ConcurrentBagclass ConcurrentBag<T> { // Each thread has its own dedicated list (no lock!) private ThreadLocal<List<T>> threadList = new ThreadLocal<>();
// Shared queue (backup) private CopyOnWriteArrayList<T> sharedList;
T borrow() { // Step 1: Search in my dedicated list (no lock!) List<T> list = threadList.get(); for (T item : list) { if (item.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return item; } }
// Step 2: Search in shared list for (T item : sharedList) { if (item.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return item; } }
return null; }}Key insight: Lock contention is minimized by using per-thread dedicated lists.
Source: HikariCP GitHub - ConcurrentBag implementation, jOOQ Blog - Brett Wooldridge Shows What it Takes to Write the Fastest Java Connection Pool
3.2 Removing Unnecessary Features
Why It Doesn’t Cache PreparedStatements
Most JDBC drivers (PostgreSQL, Oracle, MySQL, etc.) already cache at the driver level.
// MySQL Connector/J already cachesConnection conn = dataSource.getConnection();// Already cached internally by the driver!PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");Caching again at the connection pool level would be redundant and only add overhead. HikariCP boldly removed this.
Source: HikariCP GitHub - About Pool Sizing, MySQL Connector/J Connection Pooling
4. HikariCP Configuration Complete Guide
4.1 Default Values
| Setting | Default | Description |
|---|---|---|
maximumPoolSize | 10 | Maximum number of connections |
minimumIdle | Same as maximumPoolSize | Minimum number of idle connections |
connectionTimeout | 30 seconds | Wait time to acquire a connection |
idleTimeout | 10 minutes | Time before idle connections are removed |
maxLifetime | 30 minutes | Maximum connection lifetime |
keepaliveTime | 2 minutes | Connection keepalive check interval |
validationTimeout | 5 seconds | Connection validation timeout |
4.2 Spring Boot Configuration Example
spring: datasource: hikari: # Connection pool size maximum-pool-size: 20 minimum-idle: 20
# Timeouts connection-timeout: 3000 # 3 seconds validation-timeout: 1000 # 1 second
# Lifecycle max-lifetime: 580000 # 9 min 40 sec (shorter than DB wait_timeout) idle-timeout: 600000 # 10 minutes keepalive-time: 30000 # 30 seconds
# Other auto-commit: true leak-detection-threshold: 60000 # 60 seconds4.3 Important Settings
4.3.1 maximumPoolSize and minimumIdle
Recommendation: Set them to the same value!
hikari: maximum-pool-size: 20 minimum-idle: 20 # Same value!Why:
- Prevents the idle connection reduction feature from activating
- For systems that are always active like game servers or live streaming servers, there’s no need to reduce connections
- Eliminates overhead from connection scaling up and down
4.3.2 connectionTimeout
Recommendation: 0.5-3 seconds
hikari: connection-timeout: 3000 # 3 secondsWhy:
- The default of 30 seconds is too long
- Returning an error quickly is better than no response at all
- Users won’t wait 30 seconds
4.3.3 maxLifetime
Recommendation: 2-3 seconds shorter than the DB’s wait_timeout
hikari: max-lifetime: 580000 # 9 min 40 secWhy:
- MySQL’s default
wait_timeoutis 8 hours (28,800 seconds) - But in production, it’s often set to around 10 minutes (600 seconds)
- Setting HikariCP’s
max-lifetimeshorter than the DB timeout prevents the DB from disconnecting first
-- Check wait_timeout in MySQLSHOW VARIABLES LIKE 'wait_timeout';
-- Set wait_timeout (10 minutes)SET GLOBAL wait_timeout = 600;SET GLOBAL interactive_timeout = 600;4.3.4 leakDetectionThreshold
Recommendation: 60 seconds
hikari: leak-detection-threshold: 60000 # 60 secondsWhy:
- Too short and normal transactions will be flagged as leaks
- Too long and actual leaks won’t be detected
- 60 seconds is long enough for most normal transactions to complete
// Connection leak example@Serviceclass UserService { @Autowired private DataSource dataSource;
void badMethod() throws SQLException { Connection conn = dataSource.getConnection(); // Execute query Statement stmt = conn.createStatement(); stmt.executeQuery("SELECT * FROM users");
// Problem: conn.close() was never called! // After 60 seconds, HikariCP outputs a warning log // WARN - Connection leak detection triggered }}Source: HikariCP Configuration - HikariCP configuration guide, Spring Boot HikariCP
5. How Do You Determine Connection Pool Size?
5.1 The Famous Formula
There’s a formula from the HikariCP wiki:
connections = (core_count x 2) + effective_spindle_countcore_count: Number of CPU coreseffective_spindle_count: Number of concurrent disk I/O operations the DB server can handle
Example:
- 8-core CPU, 1 HDD
- connections = (8 x 2) + 1 = 17
5.2 Why “core_count x 2”?
It’s because of the speed difference between CPU and disk/network.
CPU: 1 GHz = 1 billion cycles/secDisk: 100 IOPS = 100 ops/secNetwork: 1ms latency = 1000 ops/sec
Speed difference: approximately 1 million times!While the CPU waits for disk or network, it can process other threads. That’s why you need more connections than cores.
// Analyzing connection work timeclass ConnectionWorkload { void processRequest() { businessLogic();
// 2. DB query: 10ms (I/O wait) executeQuery();
// 3. CPU work: 0.1ms processResult();
// Total time: 10.2ms // Actual CPU usage: 0.2ms (~2%) // I/O wait: 10ms (~98%) }}The CPU is idle 98% of the time! That’s why 2 or more connections per CPU core is efficient.
5.3 How Is It Actually Determined?
The formula is just a reference — actual measurement is what matters!
Step 1: Check Thread Count
server: tomcat: threads: max: 200 # Tomcat max threadsStep 2: Check Connections Needed Per Task
@Serviceclass OrderService { @Transactional void createOrder(Order order) { // Uses only 1 connection orderRepository.save(order); itemRepository.updateStock(order.getItemId()); }}
@Serviceclass ComplexService { void complexOperation() { // Problem: requires 2 connections! Connection conn1 = dataSource.getConnection(); // 1st // ... working ... Connection conn2 = dataSource.getConnection(); // 2nd // Deadlock risk! }}Step 3: Apply the Formula
pool_size = thread_count x (connections_per_task - 1) + 1Example:
- Tomcat threads: 200
- Connections per task: 1
- pool_size = 200 x (1 - 1) + 1 = 1?!
That doesn’t make sense. Why?
Step 4: Consider Actual Concurrency
Just because there are 200 Tomcat threads doesn’t mean all 200 are using the DB simultaneously.
@RestControllerclass StreamingController { @GetMapping("/api/stream/{id}") void getStream(@PathVariable Long id) { // 1. DB query (uses connection) Stream stream = streamRepository.findById(id);
// 2. Business logic (no connection used) processStream(stream);
// 3. Cache check (no connection used) cacheService.get(id);
// 4. External API call (no connection used) notificationService.send(stream); }}In reality, assuming only about 10-20% are using the DB simultaneously:
- 200 x 0.2 = 40
5.4 Netmarble’s Game Server Configuration
According to the Netmarble tech blog, for game servers they configured:
hikari: maximum-pool-size: 50 # Based on CPU core count, determined by performance testing minimum-idle: 50 # Same as maximumKey points:
- Start with the CPU core count as the initial value
- Adjust through performance testing
- Monitor and find the optimal value
Source: HikariCP GitHub - About Pool Sizing, Netmarble - HikariCP Options and Recommended Settings for Game Servers, Carrey’s Tech Blog - Considerations When Setting HikariCP Maximum Pool Size
6. When Will the Server Crash?
6.1 Scenario 1: Connection Pool Exhaustion
Situation: Traffic suddenly spikes.
// Configurationhikari: maximum-pool-size: 10 connection-timeout: 3000 // 3 seconds
// Situation// 100 requests per second// Each request takes 5 seconds for DB query
// After 1 second: 100 requests competing for 10 connections// 90 requests waiting in queue// After 3 seconds: connection-timeout triggers!
// Logorg.springframework.dao.DataAccessResourceFailureException: Unable to acquire JDBC ConnectionCaused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3000ms.Solutions:
- Increase connection pool size
hikari: maximum-pool-size: 50 # 10 → 50- Optimize queries
-- Before: 5 secondsSELECT * FROM streams WHERE status = 'LIVE';
-- After: 0.1 seconds (added index)CREATE INDEX idx_stream_status ON streams(status);SELECT * FROM streams WHERE status = 'LIVE';- Introduce caching
@Serviceclass StreamService { @Cacheable("liveStreams") List<Stream> getLiveStreams() { // Reduces DB query frequency return streamRepository.findByStatus(StreamStatus.LIVE); }}6.2 Scenario 2: Connection Leak
Situation: Connections were not returned.
@Serviceclass BadService { @Autowired private DataSource dataSource;
void leakyMethod() throws SQLException { Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// Processing...
// Problem: close() was never called! // conn, stmt, rs are all left open }}
// Call this method 10 times?// The connection pool (10 connections) is completely exhausted!Symptoms:
WARN - Connection leak detection triggered for connectionSolutions:
- Use try-with-resources (Java 7+)
@Serviceclass GoodService { @Autowired private DataSource dataSource;
void goodMethod() throws SQLException { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
// Processing...
} // close() is called automatically! }}- Use JPA/MyBatis
@Serviceclass BetterService { @Autowired private UserRepository userRepository;
void betterMethod() { // JPA manages connections automatically List<User> users = userRepository.findAll(); }}- Set up leak detection
hikari: leak-detection-threshold: 60000 # 60 seconds6.3 Scenario 3: DB Server Drops the Connection
Situation: MySQL’s wait_timeout expired.
-- MySQL default settingsSHOW VARIABLES LIKE 'wait_timeout';-- wait_timeout = 28800 (8 hours)
-- But often set shorter in productionSET GLOBAL wait_timeout = 60; -- Changed to 60 seconds// 1. Acquire connectionConnection conn = dataSource.getConnection();
// 2. Wait 70 seconds doing nothingThread.sleep(70000);
// 3. Try to execute querystmt.executeQuery("SELECT * FROM users");// Error!// Communications link failure// The last packet successfully received from the server// was 70,000 milliseconds agoSolutions:
- Set HikariCP maxLifetime
hikari: max-lifetime: 50000 # 50 seconds (shorter than wait_timeout)- Increase DB wait_timeout
SET GLOBAL wait_timeout = 600; -- 10 minutesSET GLOBAL interactive_timeout = 600; -- 10 minutes- Set keepaliveTime (HikariCP 4.0+)
hikari: keepalive-time: 30000 # Check connection every 30 secondsHow keepaliveTime works:
// HikariCP internal behaviorclass HikariPool { void keepalive() { for (Connection conn : idleConnections) { if (idleTime > keepaliveTime) { // Check connection with a simple query conn.isValid(validationTimeout); // or stmt.execute("SELECT 1"); } } }}Source: Hudeop’s Tech Blog - Does HikariCP have a test-while-idle equivalent?, SK C&C - Solving Connection Drop Issues in MySQL/MariaDB
6.4 Scenario 4: Deadlock
Situation: Deadlock caused by insufficient connection pool.
@Serviceclass DeadlockService { @Transactional void problemMethod() { // First connection acquired Connection conn1 = dataSource.getConnection();
// ... working ...
// Attempting second connection // But there are no connections left in the pool! Connection conn2 = dataSource.getConnection(); // Waits forever... (deadlock!) }}Conditions for occurrence:

Solutions:
- Apply pool size formula
# pool_size = thread_count x (connections_per_task - 1) + 1# 16 x (2 - 1) + 1 = 17
hikari: maximum-pool-size: 17- Reduce connections per task
@Serviceclass FixedService { @Transactional void fixedMethod() { // Consolidated into a single transaction // Uses only 1 connection orderRepository.save(order); itemRepository.updateStock(itemId); }}6.5 Scenario 5: Out of Memory
Situation: Too many connections causing memory shortage.
// Memory usage per connectionclass ConnectionMemory { // TCP socket: ~64KB // ResultSet buffer: ~1MB // Statement objects: ~100KB // Metadata: ~100KB
// Total: approximately 1~2MB per connection}
// 1000 connections = 1~2GB memory!hikari: maximum-pool-size: 1000 // Dangerous!Solutions:
- Maintain appropriate pool size
hikari: maximum-pool-size: 50 # Reasonable level- Check DB server resources
-- Check MySQL max connectionsSHOW VARIABLES LIKE 'max_connections';-- max_connections = 151 (default)
-- Check current connectionsSHOW STATUS LIKE 'Threads_connected';- Distribute across application servers
# 1 server: 50 connections# 3 servers: 150 connections# DB max_connections: 200
# Set with margin7. Real-World Failure Cases
7.1 KakaoPay: R2DBC Connection Pool Not Created
Problem: In Spring WebFlux + R2DBC, the connection pool was not initialized when running as a jar file.
// Characteristics of r2dbc-pool// Does not create connections at creation time,// Creates them when actually needed (Lazy Initialization)
// Works in IntelliJ (why?)// → Actuator's HealthCheck is automatically called via JMX// → This process initializes the connection pool
// Doesn't work when running as jar// → HealthCheck is not automatically called// → Connection pool is not initialized// → Delay on first request!Solution:
// Explicitly call warmupr2dbcPool.warmup().block()Lesson:
- Verify differences between development and production environments
- IDE automation features can distort actual behavior
- When adopting new technology, verify expected behavior matches actual behavior
7.2 SK C&C: MySQL wait_timeout Problem
Problem: Setting wait_timeout to 60 seconds caused connections to keep disconnecting.
-- Incorrect settingSET GLOBAL wait_timeout = 60; -- Too short!// Connections drop every 60 seconds// HikariCP fails to detect the broken connections// Application errors occur!
// Error logCommunications link failureThe last packet successfully received from the serverwas 70,000 milliseconds agoSolution:
# HikariCP settingshikari: max-lifetime: 50000 # 50 seconds (shorter than wait_timeout)
# MySQL settingswait_timeout: 180 # 3 minutesLesson:
- Be careful with DB timeout settings
- HikariCP’s
max-lifetimeshould be shorter than DBwait_timeout - Timeouts that are too short degrade performance
7.3 Personal Project: Connection Leak
Problem: Setting leak-detection-threshold to 2 seconds caused false positives.
# Incorrect settinghikari: leak-detection-threshold: 2000 # 2 seconds (too short!)@Serviceclass SchedulerService { @Scheduled(fixedDelay = 1000) void scheduledTask() { // Normal task that takes 3 seconds userRepository.findAll(); // 3 seconds
// HikariCP falsely flags as a leak! // WARN - Apparent connection leak detected }}Solution:
hikari: leak-detection-threshold: 60000 # 60 secondsLesson:
- Too short a leak detection time causes false positives
- Normal transaction processing time must be considered
- 60 seconds is generally appropriate
7.4 Productivity Loss: MySQL AbandonedConnectionCleanupThread
Problem: DB connections were being created abnormally, increasing memory usage.
// MySQL Connector/J issue// AbandonedConnectionCleanupThread keeps being created// Memory leak occurs!
// Thread Dump"MySQL-AB-1" daemon"MySQL-AB-2" daemon"MySQL-AB-3" daemon... (hundreds)Solution:
# Add JVM option-Dcom.mysql.cj.disableAbandonedConnectionCleanup=trueLesson:
- Regular services don’t manage connections directly
- JDBC driver’s automatic cleanup feature can actually be harmful
- Disable unnecessary features
Source: KakaoPay - The Case of the Missing R2DBC Connection Pool, SK C&C - Solving Connection Drop Issues in MySQL/MariaDB, velog - DB Connection Pool Leak Issue
8. Monitoring and Optimization
8.1 HikariCP Metrics
HikariCP provides various metrics.
# Actuator settingsmanagement: endpoints: web: exposure: include: metrics,health metrics: enable: hikaricp: true// Available metricshikaricp.connections.active // Active connection counthikaricp.connections.idle // Idle connection counthikaricp.connections.pending // Pending request counthikaricp.connections.timeout // Timeout occurrence counthikaricp.connections.usage // Connection usage time8.2 Prometheus + Grafana
scrape_configs: - job_name: 'spring-app' metrics_path: '/actuator/prometheus' static_configs: - targets: ['localhost:8080']Key metrics to know:
- active / maximum: Consider increasing pool size if over 80%
- pending: If non-zero, pool is insufficient
- timeout: If occurring continuously, optimize queries or increase pool
- usage: If average usage time is long, optimize queries
8.3 Optimization Checklist
Step 1: Configuration Check
- Is
maximumPoolSize=minimumIdle? - Is
connectionTimeoutset to 3 seconds or less? - Is
maxLifetime< DBwait_timeout? - Is
leakDetectionThresholdset to 60 seconds?
Step 2: Code Check
- Using try-with-resources?
- Using JPA/MyBatis? (avoid direct JDBC usage)
- Not using multiple connections in a single transaction?
Step 3: Query Optimization
- Identified slow queries?
- Using indexes appropriately?
- Resolved N+1 query problems?
Step 4: Monitoring
- Collecting HikariCP metrics?
- Set up alerts? (active > 80%, timeout > 0)
- Monitoring the DB server as well?
9. Summary
9.1 Key Takeaways
-
Creating connections is expensive (20-50ms)
- Reuse them with a connection pool
-
HikariCP is the best
- Default pool since Spring Boot 2.0+
- Thousands of times faster than alternatives
-
Configuration matters
maximumPoolSize=minimumIdleconnectionTimeout= 3 secondsmaxLifetime< DBwait_timeout
-
Pool sizing formula
connections = (core_count x 2) + effective_spindle_countBut actual measurement is more important!
-
Major failure causes
- Connection pool exhaustion
- Connection leak (not closing)
- DB timeout
- Deadlock
-
Solutions
- Use try-with-resources
- Use JPA/MyBatis
- Optimize queries
- Monitor
9.2 Practical Configuration Example
# Live streaming server configurationspring: datasource: hikari: # Connection pool maximum-pool-size: 50 minimum-idle: 50
# Timeouts connection-timeout: 3000 validation-timeout: 1000
# Lifecycle max-lifetime: 580000 # 9 min 40 sec idle-timeout: 600000 # 10 minutes keepalive-time: 30000 # 30 seconds
# Leak detection leak-detection-threshold: 60000
# MySQL settings# wait_timeout = 600 (10 minutes)# interactive_timeout = 600 (10 minutes)9.3 Closing Thoughts
After studying connection pools following thread pools, I can now see the full picture of how a live streaming server handles concurrent requests.

Next, I want to study caching and asynchronous processing further, re-establish my CS fundamentals, and work toward building faster and more stable servers.
References
Official Documentation
- HikariCP GitHub - HikariCP official repository
- HikariCP Configuration - HikariCP configuration guide
- MySQL Connector/J Connection Pooling - MySQL JDBC connection pooling
- Apache Tomcat JDBC Pool - Tomcat JDBC Pool documentation
- PostgreSQL JDBC Connection Pools - PostgreSQL JDBC connection pools
- Spring Boot HikariCP - Spring Boot official docs
Tech Blogs
- Netmarble - HikariCP Options and Recommended Settings for Game Servers - Game server optimization case
- KakaoPay - The Case of the Missing R2DBC Connection Pool - R2DBC connection pool failure case
- SK C&C - Solving Connection Drop Issues in MySQL/MariaDB - wait_timeout troubleshooting
- Carrey’s Tech Blog - Considerations When Setting HikariCP Maximum Pool Size - Pool size calculation for deadlock prevention
- Hudeop’s Tech Blog - Does HikariCP have a test-while-idle equivalent? - HikariCP connection management
- jOOQ Blog - Brett Wooldridge Shows What it Takes to Write the Fastest Java Connection Pool - HikariCP development story
Additional Resources
- hudi.blog - Database Connection Pool and HikariCP - Connection pool basics
- velog - DB Connection Pool Leak Issue - Leak detection configuration issues
- haon.blog - HikariCP and DBCP Optimization - Connection pool optimization theory
- Baeldung - A Simple Guide to Connection Pooling in Java - Java connection pooling guide
- Progress - JDBC Connection Pooling in Java Tutorial - JDBC connection pooling tutorial
Benchmarks
- HikariCP Benchmark - HikariCP official benchmark
- Medium - Database Connection Pool Analysis - Connection pool performance comparison