TechFeedTechFeed
Programming Languages

자바 가상 스레드, 스레드 풀, 히카리, 블로킹 IO | 커넥션 풀은 왜 같이 키우나?

자바 가상 스레드를 스프링 부트에서 켤 때 히카리 풀과 톰캣 스레드, synchronized 고정, 스레드 로컬을 같은 표에 둔다. 자바 21과 24, 스프링 부트 3.2 플래그, keep-alive, jcmd 덤프, 피닝 추적, JDBC 블로킹, 개발자, 백엔드, API 기준으로 켠 뒤에 느려지는 자리를 나눈다. 코틀린 코루틴 글과 각을 섞지 않는다.

by

자바 21에서 가상 스레드를 켰는데 응답이 더 느려진 적 있으신가요. 켜는 줄은 스프링 부트 3.2부터 spring.threads.virtual.enabled=true 한 줄입니다. 톰캣 스레드 200이 풀리면서 히카리 풀이 먼저 바닥납니다. 플랫폼 스레드가 막던 대기열이 커넥션 대기로만 옮겨 갑니다. 공식 문서는 자바 24 이상을 강하게 권합니다. synchronized 고정이 21에서는 캐리어를 붙잡고, 스케줄 잡은 데몬이라 스프링은 keep-alive를 같이 켜라고 적습니다. 저는 플래그보다 풀 크기와 고정 로그를 먼저 봅니다.


풀이 고갈된 새벽은 피지바운서 케이스를, 코루틴은 코틀린 스프링을, 고루틴은 고 동시성을 보면 자리가 겹치지 않습니다. 숫자는 2026년 8월 공개 문서 기준입니다.


플래그만 켠 밤에 히카리가 먼저 바닥난다

켜는 순간 요청마다 가상 스레드가 생기고, 디비를 기다리는 줄이 히카리 기본값 10에 몰립니다. 톰캣 최대 스레드 200이 자연 상한처럼 보이던 자리는 사라집니다. 스프링 부트 문서는 가상 스레드를 켜면 스레드 풀을 조절하던 속성이 더 이상 효과가 없다고 적습니다. 스케줄은 JVM 전역 플랫폼 풀에서 돌아가고, 전용 톰캣 풀에 기대던 숫자가 비기 때문입니다.


국내 팀에서 본 구성은 흔합니다. 스프링 부트 3.3, 자바 21, 아마존 알디에스 포스트그레, 히카리 10, 인스턴스 2대. 피크에 동시 요청이 80을 넘기면 액추에이터의 활성 커넥션이 10에 붙고, 대기 시간이 초 단위로 뜹니다. 로그에는 스레드 고갈이 아니라 Connection is not available, request timed out만 남습니다. 플래그를 되돌리면 다시 톰캣 대기열이 막고, 디비 세션은 잠잠해집니다.


히카리 기본 최대는 10입니다. 톰캣 기본 최대는 200입니다. 가상 스레드 이전에는 200개 중 일부만 디비를 쳐서 10으로도 버텼습니다. 켠 뒤에는 요청 수만큼 디비를 두드릴 수 있습니다. 풀을 올리기 전에 알디에스 max_connections와 인스턴스 수를 곱셈으로 맞춰야 합니다. 그 계산을 빼먹은 밤은 커넥션 풀 고갈과 같은 알람이 납니다.


한 줄 가드 | 플래그를 켜기 전에 히카리 최대, 커넥션 타임아웃, 알디에스 세션 한도를 같은 표에 적습니다. 톰캣 스레드 숫자는 더 이상 디비 앞의 댐이 아닙니다.


가상 스레드는 빨라지지 않는다

제이에피 444는 가상 스레드가 코드를 더 빨리 돌리는 스레드가 아니라고 적습니다. 처리량을 올리는 자리이지, 지연을 깎는 자리가 아닙니다. CPU가 바쁜 정렬, 암호화, 이미지 변환은 코어 수를 넘긴 스레드가 도움이 되지 않습니다. 도움이 되는 자리는 동시 요청이 많고, 그 요청이 소켓과 JDBC를 기다리는 자리입니다.


그래서 벤치 숫자만 보고 켜면 실망합니다. 워밍 없이 CPU가 100%인 잡을 가상 스레드로 바꾸면 처리량은 그대로이거나 줄어듭니다. 블로킹 IO가 긴 조회, 외부 API, 파일 읽기에서만 요청당 스레드 스타일이 다시 싸집니다. 외부 호출이 한꺼번에 늘면 회로 차단기가 같이 필요합니다. 스레드가 싸졌다고 재시도를 무한히 늘리면, 이번엔 상대 서버가 먼저 죽습니다.


고루틴과 비교하면 느낌이 비슷합니다. 고도 고루틴을 풀에 넣지 말라고 가르칩니다. 제이에피도 가상 스레드를 풀에 넣지 말라고 적습니다. 비싼 자원을 나누려고 쓰던 스레드 풀 습관을 그대로 가져오면, 요청마다 커넥션을 새로 여는 코드가 나옵니다. 그 습관은 고 동시성 글의 워커 풀 자리와 다릅니다. 여기는 요청당 하나씩 만들고, 제한은 세마포어나 풀로 겁니다.


자바 백엔드 서버에서 요청과 데이터베이스 연결이 모이는 구성 장면
요청당 스레드가 싸지면 디비 풀이 다음 병목이 된다

플랫폼 200과 가상 수천을 같은 표에 올리면

고르는 기준은 로고가 아니라, 디비와 외부 호출이 요청을 얼마나 붙잡는가입니다. 아래 숫자는 공식 기본값과 2026년 8월 스프링 부트 4.1 문서 기준입니다.


항목플랫폼 스레드가상 스레드
자바 버전17에서도 동작21 이상. 스프링은 24 이상을 강하게 권함
켜는 법기본값spring.threads.virtual.enabled=true
톰캣 최대 스레드기본 200이 요청 상한처럼 동작풀 속성이 효과를 잃음. JVM 전역 스케줄
히카리 기본최대 10. 200 중 일부만 디비를 침요청이 한꺼번에 10을 때림. 숫자를 다시 맞춤
스케줄 잡플랫폼 스레드가 JVM을 붙듦데몬이라 spring.main.keep-alive=true를 권함
잘 맞는 자리CPU가 바쁜 배치, 짧은 요청블로킹 조회와 외부 호출이 긴 API

스프링 부트 가상 스레드 절은 스프링 애플리케이션 문서에 있습니다. 자바 쪽 정의는 제이에피 444오라클 가상 스레드 문서입니다. 히카리 기본값은 히카리 설정 칸에 최대 10으로 적혀 있습니다.


스테이징에서 플래그와 풀, keep-alive를 같이 켠다
# application.yml spring: threads: virtual: enabled: true main: keep-alive: true datasource: hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 2000 leak-detection-threshold: 15000

30은 만능이 아닙니다. 인스턴스가 3대면 디비 앞에는 90이 섭니다. 알디에스 기본 100에서 슈퍼유저와 모니터링을 빼면 여유가 없습니다. 피지바운서를 트랜잭션 모드로 두면 앱 풀을 올려도 디비 세션은 고정됩니다. 숫자를 올리기 전에 pg_stat_activity의 대기 이벤트를 봅니다. 대기 이벤트가 Client가 아니라 락이면, 풀을 올려도 느립니다.


synchronized와 네이티브가 캐리어를 붙잡는 자리

자바 21에서는 synchronized 블록 안에서 블로킹이 나면 가상 스레드가 캐리어에 고정됩니다. 네이티브 호출과 외국 함수도 같습니다. 고정 자체가 틀린 결과는 아닙니다. 다만 그 동안 운영체제 스레드 하나가 놀지 못합니다. 자주, 길게 붙으면 처리량이 떨어집니다. 스프링이 자바 24를 강하게 권하는 이유 중 하나가 여기입니다. 제이에피 491이 자바 24에서 synchronized 고정을 거의 걷어냈습니다.


자바 21을 당장 못 올리는 팀은 긴 아이오를 감싼 synchronized를 리엔트런트락으로 바꿉니다. 시작 때만 도는 초기화나 메모리 연산은 그대로 둬도 됩니다. 고정이 의심되면 스테이징에서 -Djdk.tracePinnedThreads=short를 켭니다. JFR 이벤트 jdk.VirtualThreadPinned는 기본 임계가 20ms입니다. 덤프는 옛 jstack이 아니라 아래 명령을 씁니다.


고정 추적과 가상 스레드 덤프
java -Djdk.tracePinnedThreads=short -jar app.jar jcmd <pid> Thread.dump_to_file -format=json /tmp/threads.json # 덤프에 가상 스레드가 보이면 켠 것이다. 옛 jstack 목록에는 플랫폼만 남는다

스레드 로컬도 같이 봅니다. 가상 스레드는 요청마다 생기므로, 커넥션이나 큰 버퍼를 스레드 로컬에 넣어 재사용하는 패턴은 메모리만 돕니다. 제이에피는 그 습관을 버리라고 적습니다. 로그 엠디씨처럼 짧은 값은 당장 깨지지 않습니다. 다만 요청마다 큰 객체를 붙이면 힙이 먼저 경고합니다. 코틀린 코루틴의 스레드 로컬 전파와 다른 함정입니다. 코루틴은 디스패처를 나누는 글을 보면 됩니다.


스레드 덤프와 모니터링 화면이 떠 있는 개발 워크스테이션
jcmd JSON 덤프와 피닝 추적. 옛 jstack만으로는 가상 스레드가 안 보인다

스테이징에서 켤 순서

프로덕션 플래그보다 스테이징 부하가 먼저입니다. 순서는 버전 확인, 풀 표, 고정 로그, 피크의 두 배 부하, 일주일 관측입니다. 자바 21이면 고정 로그를 필수 칸에 넣고, 24 이상이면 같은 로그를 하루만 켜 둡니다. 스케줄 잡이 있는 서비스는 keep-alive를 빼먹지 않습니다.


순서할 일통과 기준
1java -version, 스프링 부트 3.2 이상21 이상. 가능하면 24
2히카리 최대 × 인스턴스 ≤ 디비 한도슈퍼유저, 모니터링 여유 20%+
3keep-alive, 피닝 추적, 누수 감지스케줄 잡이 프로세스를 끄지 않음
4피크의 두 배로 5분히카리 대기 p95, 디비 세션, 오류율
5프로덕션 카나리 한 대오류 예산 안에서만 확대. 에스엘오 글

켠 뒤 풀과 버전을 확인
java -version curl -s localhost:8080/actuator/metrics/hikaricp.connections.active curl -s localhost:8080/actuator/metrics/hikaricp.connections.pending jcmd $(pgrep -f app.jar) Thread.dump_to_file -format=json /tmp/vt.json

액추에이터 메트릭이 없다면 스테이징만 잠시 엽니다. 프로덕션에서 액추에이터를 넓히지 않습니다. 대기 pending이 0이 아닌데 활성 커넥션이 최대에 붙어 있으면 풀이 작습니다. pending이 0이고 응답만 느리면 쿼리입니다. 쿼리는 실행 계획 글로 넘깁니다. 런타임 예외 알림은 센트리처럼 요청 아이디를 남기는 쪽이 편합니다.


웹플럭스를 이미 쓰는 서비스는 이 플래그가 메인 경로가 아닙니다. 리액티브 스택은 스레드 모델이 다릅니다. 블로킹 JDBC를 웹플럭스 안에서 돌리던 팀을 가상 스레드 MVC로 되돌리는 선택은 별도 이전입니다. 이 글은 서블릿 MVC, 톰캣, 히카리를 전제로 합니다.


데이터베이스 커넥션과 부하 지표를 비교하는 표 형태의 정보 그림
히카리 최대 × 인스턴스가 알디에스 세션 한도를 넘기지 않게 맞춘다

참고 자료


런타임과 플래그 문구는 수시로 바뀝니다. 위 표는 2026년 8월 공개 페이지 기준 점검용이며, 적용의 최종 근거는 공식 화면입니다.


자주 묻는 질문

자바 17에서 플래그만 켜면 되나?

안 됩니다. 가상 스레드는 자바 21부터입니다. 17에서 속성을 넣어도 요청은 플랫폼 스레드로 남습니다. 스프링 부트 3.2 이상이어도 런타임이 21보다 낮으면 기대한 스케줄이 아닙니다. 먼저 java -version을 봅니다.


히카리를 톰캣 200에 맞추면 되나?

맞추면 디비가 먼저 죽습니다. 히카리 최대 곱하기 인스턴스 수가 알디에스 max_connections을 넘기면 새벽 알람이 납니다. 200은 옛 톰캣 상한이지 디비 한도가 아닙니다. 피지바운서를 두거나, 인스턴스당 20~40처럼 디비 표를 먼저 채웁니다.


자바 21을 당장 24로 올려야 하나?

필수는 아닙니다. 다만 스프링 문서는 24 이상을 강하게 권합니다. 21은 synchronized 안에서 블로킹이 나면 캐리어가 붙습니다. 긴 아이오를 모니터로 감싼 코드가 많으면 21에서 피닝 로그를 켜고, 올릴 수 있을 때 24로 갑니다.


웹플럭스 서비스도 이 플래그를 켜나?

메인 경로가 리액티브면 이 플래그가 주인공이 아닙니다. 서블릿 MVC에 블로킹 JDBC가 있는 자리에 맞습니다. 웹플럭스 안에서 블로킹을 숨겨 둔 코드가 있으면, 그 칸만 가상 스레드 실행기로 빼는 식은 별도 설계입니다.


스케줄 잡이 있는 배치도 켜도 되나?

켜면 스케줄 스레드가 데몬이라 프로세스가 내려갈 수 있습니다. 스프링은 spring.main.keep-alive=true를 같이 켜라고 적습니다. 배치 전용 워커는 플래그를 끄고 플랫폼 풀을 유지하는 편이 설명하기 쉽습니다.


가상 스레드를 풀에 넣어도 되나?

제이에피는 넣지 말라고 적습니다. 가상 스레드는 싸서 요청마다 만듭니다. 동시 수를 제한하려면 스레드 풀이 아니라 세마포어나 히카리 같은 자원 풀을 씁니다. 풀에 넣으면 옛 습관이 커넥션 재사용과 섞여 숫자가 안 맞습니다.


자바가상 스레드히카리스레드 풀스프링 부트JDBC백엔드API개발자톰캣Java

함께 보면 좋은 문제 해결

EXPLORE / Programming Languages

이어서 읽어보기

전체 토픽 둘러보기