우선적으로 풀이란 무엇이고 왜 해야되는가??
우선 풀(Pool)이라는 것은 수영장 풀이라고 생각해도 될 것 같다. (유튜브에서 봤었던거같은? 이해가 잘 되었다)
한마디로 우리가 수영장 풀에다 물을 미리 부어놓고 재미있게 놀 듯이 프로그램에서 자원을 미리 생성해두고 관리하는 것을 의미한다.
보통 백엔드 관점에서는 2가지로 나뉘는 것 같다.
첫번째는 데이터베이스 커넥션 풀과 쓰레드 풀로 나뉜다.
데이터베이스 커넥션 풀: DB에 연결할 때마다 새 커넥션을 맺는 대신, 일정 수의 커넥션을 미리 만들어 두고 필요할 때 꺼내 쓰는 방식
쓰레드 풀: 요청 혹은 작업을 처리할 쓰레드를 매번 새로 생성하지 않고 일정 수의 쓰레드를 유지하며 돌아가게 하는 방식
둘 다 하는 역할은 자원을 미리 생성해두는 건 똑같다.
근데 왜 쓰냐??
크게 3가지 정도로 나눌 수 있을 것 같은데
첫번째는 비용 절감이다.
쓰레드나 데이터 베이스 커넥션을 생성, 제거하는데는 큰 비용이 든다. 이걸 미리 만들어놓은면 비용을 절감할 수 있다.
두번째는 성능 향상이다.
풀에 생성만 해놓으면 바로바로 꺼내쓰면 되기에 성능상으로도 크게 이점이 생긴다.
마지막은 안정성 측면이다.
풀 사이즈를 제한해놓으면 시스템에서 허용자원을 명확하게 관리가 된다. 수영장에 물을 아무리 부어도 수영장 사이즈 이상으로는 물을 못담는거라고 생각하면 될 것 같다. 그래서 무분별하게 너무 많은 커넥션, 쓰레드가 생기는 것을 막을 수 있다.
만약 이상하게 설정하면??
그래서 우리는 풀 사이즈 조절을 잘 하면 성능상의 이점을 챙길 수 있다.
만약 너무 크거나 작으면 어떻게 될까?
너무 작으면
자원이 부족한 경우가 많을 것이다. 그래서 DB연결을 계속 대기해 응답 지연이 발생할 것이다.
너무 크면
불필요한 자원이 점유하고 관리 비용 또한 커질 것이다.
쓰레드 풀을 너무 크게 설정하게 되면 CPU의 컨섹스트 스위칭이 너무 많이 발생하여 오히러 성능이 떨어질 것이다.
그래서 우리는 적정값을 어떻게 찾을 것인가??
우선 Spring Boot에 있는 Tomcat 쓰레드 풀부터 봐보자
server:
tomcat:
accept-count: 10
max-connections: 8192
threads:
max: 10
server.tomcat.threads.max: 10
- 톰캣이 동시에 처리할 수 있는 워크 스레드(worker thread)의 최대 개수이다. 최대 10개까지 동시에 처리할 수 잇는 것을 의미한다.
- 10개를 초과하는 요청이 들어오면, accept-count만큼 대기열에 쌓이거나 더 이상 대기열이 없으면 연결이 거부될 수 있다.
server.tomcat.accept-count: 10
- 톰캣의 워크 스레드가 모두 사용 중일 때, 추가로 받을 수 있는 대기 중 연결(요청) 수입니다.
- 즉, threads.max=10이 모두 바쁠 때, 최대 10개의 요청까지 연결을 대기열에 쌓아둘 수 있습니다.
- 대기열이 가득 차면 추가 요청은 거부(Connection Refused)될 수 있으므로, 트래픽 상황을 고려해 적절히 설정해야 한다.
server.tomcat.max-connections: 8192
- 톰캣이 한 번에 처리할 수 있는 소켓 연결 총 개수의 상한입니다.
- NIO(논블로킹 I/O) 기반으로 동작할 경우, 실제로 스레드가 없어도 커넥션이 “열려만” 있을 수 있다.
- 워크 스레드는 10개뿐이지만, 동시에 열려 있을 수 있는 연결 자체는 8,192개까지 가능하다는 의미이다.
- 예: WebSocket 연결 등, 스레드가 지속적으로 붙어있지 않고 비동기로 처리 가능한 연결을 많이 열 수 있다.
그다음엔 우리가 어떤 값을 설정할 수 있는지 데이터베이스 커넥션 풀(hikariCp)도 살펴보겠다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/데이터베이스이름?useSSL=false&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 비번
hikari:
connection-timeout: 30000
maximum-pool-size: 10
max-lifetime: 1800000
validation-timeout: 5000
initialization-fail-timeout: -1
현재 진행중인 프로젝트에 있는 application.yml의 일부이다.
connection-timeout
우선 이 설정은 뭘까? 이 설정은 커넥션 풀에서 커넥션을 구하기 위해 대기하는 시간이다. 연결가능한 개수를 초과하면 대기가 발생하게 되는 시간이다. 기본값은 30초이지만 보통 너무 길기에 3초 이내로 설정해서 응답 시간을 최소화하는 방향으로 가져가야된다.
maximum-pool-size
이 설정은 풀에서 제공할 수 있는 최대 커넥션 개수이다. 수영장 풀 크기를 뜻한다. 기본 값은 10개이고 적절한 값을 설정해야된다.
max-lifetime: 1800000
각 커넥션이 풀에 살아 있을 수 있는 최대 시간(기본적으로 30분).
장기간 유지되는 커넥션이 네트워크나 DB 측에서 끊기는(‘서브넷 타임아웃’ 등) 문제를 방지하기 위해 일정 시간이 지나면 강제로 재생성하게끔 설정.
validation-timeout: 5000
커넥션 유효성 검사(Validation) 시도 시 대기할 수 있는 최대 시간입니다.
이 시간이 지나도록 검사(쿼리)가 완료되지 않으면 커넥션이 유효하지 않은 것으로 간주하고 풀에서 제외시킵니다.
initialization-fail-timeout: -1
이 설정은 크게 연관없지만 배포 도중 docker에서 mysql을 연결할때 만약 실패할 경우 종료가 되어버려서 -1을 설정하게 되면 연결에 실패하더라도 재시도를 계속 시도하는 설정이다.
설정은 어느정도 알아봤으니 우리는 어떻게 적절한 사이즈를 정할 수 있을까?
쓰레드 풀의 적절한 사이즈를 구하는 글은 여기 잘 정리되어있다.
https://code-lab1.tistory.com/269
이상적인 스레드 풀의 적정 크기에 대하여, 스레드 풀 크기 공식, 리틀의 법칙
스레드 풀의 크기를 적절히 설정해야 하는 이유 스레드를 생성하는 것은 비용이 드는 작업이다. 플랫폼마다 오버헤드는 다르지만, 스레드가 생성될 때 요청이 처리되는 지연시간(latency)과 OS에
code-lab1.tistory.com

위를 기반으로 어느정도 설정하는게 좋을까?
우선 서버의 cpu 코어 개수부터 알아보자.

현재 서버는 4코어의 cpu가 가지고 있다. 이를 통해 계산해보자
- 코어 개수: 4
- 목표 CPU 사용률: 80%(0.8)
- 대기시간/서비스시간 = 1 (대기시간이 서비스시간랑 동일)
목표 CPU는 80퍼센트로 두었고 그 이유는 서버의 안정성을 가져가기 위해선 80% 이내로 가져가야된다고 하여 잡았습니다.

https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
About Pool Sizing
光 HikariCP・A solid, high-performance, JDBC connection pool at last. - brettwooldridge/HikariCP
github.com
여기hikariCP 공식문서에 정리되어있는데 핵심 내용만 정리해보겠다.
풀 크기를 무조건 크게 잡으면 성능이 오히려 떨어진다
- 동시에 접속하는 사용자 수(예: 10,000명)가 많다고 해서 커넥션 풀을 10,000개로 늘려 두는 것은 “정말 위험한” 설정이라고 한다.
- OS나 DB가 처리할 수 있는 한계치(특히 CPU 코어 수)를 넘는 너무 많은 스레드/커넥션을 생성하면, 문맥 교환(Context Switching) 및 리소스 경합으로 인해 성능이 급격히 저하.
CPU, 디스크, 네트워크가 핵심 자원
- DB가 처리해야 할 주요 병목은 보통 CPU, 디스크, 네트워크 세 가지입니다.
- CPU 코어 수가 8이라면, 이론적으로 동시에 처리할 수 있는 쿼리 수도 8 정도입니다.
- 하지만 디스크 I/O(특히 회전식 HDD)나 네트워크 대기로 인해 스레드가 ‘Blocked’ 상태가 될 수 있으므로, “약간” 더 많은 커넥션이 필요할 수 있습니다.
- SSD처럼 빠른 디스크를 사용한다면 디스크 대기 시간이 줄어들기 때문에, 오히려 커넥션 수를 줄이는 것이 더 유리한 경우도 있습니다(블로킹 시간이 줄어 스레드 수가 많아봐야 이점이 없음).
PostgreSQL 공식 가이드: (코어 수 * 2) + 디스크 스핀들 수
- PostgreSQL 문서에서 제시한 공식: pool size = (코어 수 * 2) + effective_spindle_count
- effective_spindle_count는 디스크가 실제로 I/O를 담당해야 하는 정도를 나타낸다고 한다. 디스크가 n개 라면 + n을 더한다.
작은 풀을 ‘포화’ 상태로 쓰는 것이 일반적으로 더 낫다
- “조금 작은 풀”에서 스레드들이 잠깐씩 커넥션을 빌려 쓰고 빠르게 반환하는 방식이, “너무 큰 풀”에서 여기저기서 동시에 처리 대기하는 것보다 전체 처리량이 크게 나올 수 있습니다.
- 예시로, 4코어 i7 서버에 디스크가 1개라면 커넥션 풀 사이즈 9~10개 정도가 적절하다고 합니다.
- 실제로 10,000명 이상의 사용자가 동시에 쿼리를 던져도, 제대로 튜닝된 작은 풀에서는 수천 TPS(초당 트랜잭션)도 충분히 커버할 수 있다고 합니다.
이 같은 기준으로 쓰레드풀과 데이터베이스를 적절하게 해보자
현재 cpu 코어는 4개이다.
(코어 수 * 2) + 디스크 스핀들 수이므로
(4 * 2) + 1 = 9으로 잡고 성능 테스트를 진행해보자
부하테스트는 현재 모니터링을 Grafana로 진행중이기에 Grafana Laps에서 개발한 K6를 사용했다.
brew install k6
설치는 간단하게 한줄이면 끝난다. 홈브류 최고
우리 서비스에서 가장 주로 조회되는 API 기준으로 성능테스트를 진행했다. 가장 많은 요청은 뉴스, 공지사항, 예약 API이다.
GET /api/board
GET /api/news
GET /api/room/1/reservation/month?yearMonth=2025-03
의미있는 결과를 위해 데이터베이스에 100만건의 정보를 세팅해두고 진행했다.
SELECT COUNT(*) AS board_count FROM board;

가상의 사용자 100명이 10번씩 반복 요청하도록 script를 설정했고 각 api 마다 테스트를 진행했습니다.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
// 동시 가상 사용자(VU) 수
vus: 100,
// 전체 요청 반복 횟수 (VU * 반복 = 총 요청 수)
// 예: 100 VU * 10회 = 1,000번 요청
iterations: 1000,
};
export default function () {
// 실제 요청 보내기
const res = http.get('http://서버/api/board');
// 응답 검증 (상태 코드가 200인지 확인)
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
작성한 스크립트를 실행해보자.

maxThreadCount | maximumPoolSize | Throughput(TPS) |
6 | 1 | 119.826426/s |
6 | 2 | 217.503309/s |
6 | 3 | 221.629664/s |
6 | 4 | 227.127861/s |
6 | 5 | 230.685076/s |
6 | 6 | 231.725979/s |
10 | 5 | 232.630676/s |
10 | 6 | 234.024498/s |
10 | 7 | 243.013328/s |
10 | 8 | 248.501848/s |
10 | 9 | 245.822152/s |
10 | 10 | 244.067524/s |
20 | 9 | 256.251285/s |
20 | 10 | 246.2121385/s |
30 | 9 | 244.2123122/s |
100 | 50 | 225.595489/s |
커넥션 풀의 개수가 스레드 풀의 개수보다 적으면 무의미하기에 적절히 증가시켰다.
놀랍게도 공식으로부터 도출된 9개가 가장 높은 처리량이 나오는 걸 확인할 수 있었다.
그러나 Brian Goetz님이 적어주신 최적의 스레드 수는 6개가 나왔으나 더 많은 스레드를 설정했을때 더 높은 TPS가 나왔다.
그 이유를 좀 찾아보니 옛날의 운영체제와 최신의 운영체제의 컨텍스트 비용이 달라서 발생한 일이었다. 현대 운영체제는 CPU 스레드 컨텍스트 스위칭을 매우 최적화가 되었기 때문에 추가적인 스레드가 있다고해서 성능 저하가 크게 일어나지 않기 때문이었다.
그래서 최종적으로 가장 최적의 값이 나온 maxThreadCount 20, maximumPoolSize 9개를 사용하여 application.yml에 설정을 하였다.
결론적으로 기본 세팅인 maxThreadCount 200, maximumPoolSize 10일때보다
TPS 231 -> 256으로 10.82% 향상 시킬 수 있었다.
'Project' 카테고리의 다른 글
[Project] Spring 직렬화, 역직렬화 문제 - getWriter() has already been called for this response (1) | 2025.02.28 |
---|---|
[Project] 게시물 N+1 문제 해결기 (쿼리 40 -> 11 -> 1) (0) | 2025.01.31 |
[Project] 좋아요 로직 반정규화 및 동시성 처리 (0) | 2025.01.31 |
[Project] 세종대학교 auth & 예약 시스템 (2) | 2025.01.17 |
[Project] Multipart/form 파일, Dto 동시 요청 시 발생 에러 (2) | 2025.01.16 |