
최근 웹 서비스에서 성능 저하와 불안정한 응답 시간이 발생하여 원인을 분석하던 중, DB Connection Pool(HikariCP)과 Tomcat Thread 수의 설정이 주요한 요인으로 보였다. 이에 따라 다양한 설정 값을 적용해가며 성능을 측정하고, 최적의 조합을 찾기 위한 테스트를 수행하였다.
테스트 결과는 단순히 풀 사이즈를 크게 늘리는 것이 성능 개선으로 이어지지 않으며, 적절한 비율을 유지하는 것이 중요하다는 점을 보여준다.
Spring Boot 애플리케이션의 DB Connection Pool(HikariCP) 및 Tomcat Thread 설정 최적화
Throughput(처리량), 응답 속도, 안정성을 고려한 적정 조합 도출
대상 API: 강좌 카드 조회 API
요청 스레드 수: 1000
요청 횟수: 각 1000회
요청 방식: 인증 없이 API 호출(허용됨.)
초기 설정 (Baseline)
Tomcat threads: 10 (default)
HikariCP pool size: 10 (default)
server.tomcat.threads.max=200
spring.datasource.hikari.maximum-pool-size=159
spring.datasource.hikari.minimum-idle=159
spring.datasource.hikari.connection-timeout=500
spring.datasource.hikari.validation-timeout=1000
spring.datasource.hikari.max-lifetime=1800000
평균 응답시간: 1488ms
Throughput: 670 req/s
P95, P99 지연시간이 크게 악화
Too many connections 에러 발생
풀 사이즈를 과도하게 늘리면 DB 경합과 컨텍스트 스위칭으로 꼬리 지연이 심해진다.
DB 커넥션이 많긴 하지만 생성된 요청 스레드 수가 더 많아서 기다리는 상황이 발생한다. 또, MySQL의 설정을 확인할 필요성을 느꼈다.
| 변수명 | 값 |
|---|---|
| max_connections | 151 |
| Threads_connected | 146 |
| Max_used_connections | 152 |
| wait_timeout | 28800 |
| interactive_timeout | 28800 |
SET GLOBAL max_connections = 220; 적용 → 여전히 Too many connections 발생
SET GLOBAL max_connections = 220;
위 명령어 실행으로 커넥션 수를 조정하고 실행했으나, 에러가 발생했고 확인해보니 값이 원상 복구 되어있었음.

Docker 환경에서 영구 반영 필요했고, CI/CD 파이프라인에 해당 커맨드를 수행하도록 명령어를 수정하였음.

현재 백엔드 아키텍처 상에서 Websocket server도 존재하며, 이 서버도 역시 MySQL에 접근하기 때문에 모든 Connection을 할당하는 것은 바람직하지 않다고 여겨짐.
Default값이 10 이므로, 웹소켓 서버에 10을 할당하고, API 서버에 141을 할당함.
| Label | Samples | Average (ms) | Min (ms) | Max (ms) | Std. Dev (ms) | Error % | Throughput (req/s) | Received KB/s | Sent KB/s | Avg. Bytes |
|---|---|---|---|---|---|---|---|---|---|---|
| HTTP Request | 753,698 | 1,488 | 6 | 4,900 | 704.71 | 0.00133 | 670.45 | 1,016.48 | 123.58 | 1,552.52 |
| 항목 | 평가 | 코멘트 |
|---|---|---|
| 처리량(Throughput) | 준수 | 약 670 req/s 처리 |
| 평균 응답시간 | 보통 | ~1.49s (서비스 특성에 따라 체감) |
| 분산(Std. Dev) | 높음 | ~705ms → 응답 편차 큼 |
| Tail Latency | 주의 | Max ~4.9s → 일부 사용자 지연 |
| 안정성(Error%) | 매우 우수 | 0.0013% 수준의 극저 에러율 |
풀사이즈를 크게 잡을수록 DB 경합과 컨텍스트 스위칭 증가로 인해 P95+가 증가할 수 있다.
권장 튜닝 원칙 = 스레드 : 커넥션 = 2 ~ 3 : 1
풀 사이즈는 필요 동시 DB 작업 수 만큼만 필요함.
처리량을 중시한다면, Pool size를 높게 잡음(80정도)
Tail latency 최적화가 목적이라면, Pool size를 줄임. ⇒ 경합을 완화하여 개선함.
server.tomcat.threads.max=280
spring.datasource.hikari.maximum-pool-size=60
spring.datasource.hikari.minimum-idle=24
spring.datasource.hikari.connection-timeout=3000
HikariPool-1 - Connection is not available, request timed out after 3002ms
(total=24, active=24, idle=0, waiting=224)
Too many connections
여전히 Too many connections 에러 발생함.
Too many connections 에러 발생함.

| Label | Samples | Avg | Median | 90% Line | 95% Line | 99% Line | Min | Maximum | Error % | Throughput | Received KB/sec | Sent KB/sec |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| HTTP Request | 121706 | 1759 | 1393 | 3347 | 3543 | 4236 | 2 | 9202 | 0.00822 | 566.61 | 863.43 | 103.72 |
초반 지표가 너무 나빠서 조기종료.
일부 조합에서 성능 개선이 있었지만, 전체적으로 큰 차이는 없었음.
그러나, tomcat에서 생성되는 요청 처리용 스레드들이 커넥션을 할당받기를 대기하다가 timeout이 발생하여 실패하는 것을 통해 thread 개수와 connection 수를 맞춰줘야할 필요성을 느낌.
server.tomcat.threads.max=200
spring.datasource.hikari.maximum-pool-size=200
spring.datasource.hikari.minimum-idle=50
결과: 여전히 Too many connections 발생
server.tomcat.threads.max=64
spring.datasource.hikari.maximum-pool-size=64
spring.datasource.hikari.minimum-idle=32
spring.datasource.hikari.connection-timeout=3000
| Label | Samples | Avg | Median | 90% Line | 95% Line | 99% Line | Min | Maximum | Error % | Throughput | Received KB/sec | Sent KB/sec |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| default | 752670 | 1494 | 1445 | 1969 | 2060 | 2558 | 7 | 7383 | 0.00132% | 667.30 | 1011.72 | 123.00 |
| 커넥션 풀 조정 (max = 159, idle = 159) | 753698 | 1488 | 1207 | 3092 | 3390 | 3731 | 6 | 4900 | 0.00133% | 670.45 | 1016.48 | 123.58 |
| 개선율 (커넥션 풀 조정 vs default) | - | -0.40% | -16.5% | +57.0% (악화) | +64.6% (악화) | +45.8% (악화) | -14.3% | -33.6% | 거의 동일 | +0.47% | +0.47% | +0.47% |
| t.m=64, p.s=64, s.m=32 | 1000000 | 1479 | 1415 | 1752 | 1890 | 2180 | 26 | 3270 | 0.0% | 673.52 | 1020.15 | 124.31 |
| 개선율 (t.m=64, p.s=64, s.m=32 vs default) | - | -1.0% | -2.1% | -11.0% | -8.3% | -14.8% | +271% (↑) | -55.7% | 개선 (0%) | +0.93% | +0.84% | +1.07% |
default 대비 평균 응답시간 1% 감소, 중앙값은 2.1% 감소.
눈에 띄는 차이는 아니지만, 일관된 응답 속도 개선을 확인할 수 있었다.
90% Line: 11% 개선
95% Line: 8.3% 개선
99% Line: 14.8% 개선
즉, 전체적인 체감 성능보다는 고부하 상황에서 극단값 지연(Tail Latency)이 줄어든 점이 의미 있다.
default: 667 req/s → 최적 설정: 674 req/s
0.93% 개선으로 크지는 않지만, 안정적으로 더 많은 요청을 처리할 수 있게 됨.
default: 0.00132% → 최적 설정: 0.0%
에러가 완전히 제거됨. 안정성 측면에서 큰 개선.
단순히 커넥션 수를 늘리면, 더 많은 스레드가 할당받아 사용할 수 있을 것으로 생각했으나 틀렸다. MySQL의 내부 설정도 고려해야 한다는 점을 배울 수 있었다.
특히, 관련 자료 조사 과정에서 오히려 커넥션 수를 크게 줄이는 것이 전체 성능을 대폭 향상시킨 사례를 접했는데, 이전에 학부 수업에서 배웠던 병렬처리에서 context switching 비용을 상기할 수 있어 좋았다.
이번 테스트를 기반으로 시스템의 전반적인 조회 성능을 향상시킬 수 있었다. 특히 꼬리 지연을 감소시킴으로써 일반적인 응답시간 감소를 이끌어낼 수 있었다.