Contents
see List가상 스레드는 언제 효과적인가
Java 가상 스레드(virtual thread)는 요청마다 작업 스레드를 배정하는 서버에서 동시 접속 수를 늘리기 위한 실행 단위다. 운영체제 스레드를 무한히 늘리는 방식이 아니라 JVM이 많은 가상 스레드를 적은 수의 플랫폼 스레드에 효율적으로 연결한다. 따라서 데이터베이스 조회, 외부 HTTP 호출, 파일 I/O처럼 응답을 기다리는 시간이 많은 업무에 특히 잘 맞는다.
전통적인 고정 스레드 풀은 스레드 수를 낮게 잡으면 대기 요청이 쌓이고, 높게 잡으면 메모리와 문맥 전환 비용이 커진다. 가상 스레드는 이 선택을 단순하게 만들지만, 모든 성능 문제가 자동으로 해결되는 것은 아니다. CPU 계산이 길거나 공유 락 경합이 심한 작업은 병렬 처리량이 CPU 코어 수를 넘지 않는다. 도입 전에는 서비스가 I/O 대기 중심인지, 연결 풀과 외부 시스템이 병목인지부터 구분해야 한다.
적용 전 확인할 조건
- Java 21 이상을 사용하고, 실제 실행 JDK도 같은 버전인지 확인한다.
- 블로킹 JDBC, HTTP 클라이언트, 파일 처리처럼 요청 단위가 독립적인 I/O 작업을 우선 대상으로 삼는다.
- DB 커넥션 수, HTTP 연결 수, 외부 API 호출량에는 별도의 상한이 필요하다.
- ThreadLocal에 큰 객체나 사용자 데이터를 오래 보관하지 않는다.
- 공유 synchronized 블록, 네이티브 호출, 장시간 점유 락은 가상 스레드 이점을 줄일 수 있으므로 측정한다.
요청 단위 작업을 가상 스레드로 실행하기
가장 안전한 시작점은 기존 비동기 프레임워크를 전면 교체하지 않고, 독립된 배치 작업이나 외부 연동 호출부터 가상 스레드 실행기를 적용하는 것이다. ExecutorService를 try-with-resources로 열면 작업 완료 뒤 자원을 정리할 수 있다. 아래 예시는 여러 고객 시스템의 상태를 조회하는 형태다.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var futures = customerIds.stream().map(id -> executor.submit(() -> client.send(requestFor(id), HttpResponse.BodyHandlers.ofString()))).toList(); for (var future : futures) { System.out.println(future.get().statusCode()); } }예제의 핵심은 작업 수만큼 가상 스레드를 만드는 것이지, 외부 API를 무제한 호출하는 것이 아니다. timeout을 지정하고, 실패를 한 요청의 예외로 분리하며, 서비스별 동시 요청 상한을 둬야 한다. 특히 수백 개 요청을 한 번에 실행하면 상대 시스템의 속도 제한과 자체 연결 풀이 먼저 문제가 될 수 있다.
동시성 제한은 별도로 설계한다
가상 스레드를 사용해도 데이터베이스 커넥션 풀의 크기는 무작정 키우지 않는다. DB는 제한된 연결 안에서 안정적으로 처리되도록 두고, 애플리케이션에서 대기하는 가상 스레드를 허용하는 편이 예측 가능하다. 외부 API처럼 호출량 제한이 있는 자원에는 Semaphore로 명시적인 동시성 경계를 둔다.
private final Semaphore partnerLimit = new Semaphore(20); String callPartnerApi(String id) throws Exception { partnerLimit.acquire(); try { return partnerClient.fetch(id); } finally { partnerLimit.release(); } }이 방식은 동시에 20개까지만 실제 호출을 통과시키고, 나머지 요청은 값비싼 플랫폼 스레드를 점유하지 않은 채 기다릴 수 있게 한다. 제한 값은 추측으로 정하지 말고 외부 API의 계약, 평균 응답 시간, 오류율, DB 풀 대기 시간으로 조정한다. 실패 재시도도 같은 제한 안에서 실행해야 일시 장애 때 트래픽이 증폭되는 것을 막을 수 있다.
피해야 할 사용 패턴
가상 스레드를 기존의 CPU 작업용 ForkJoinPool 대체재로 보면 안 된다. 이미지 변환, 대규모 암호화, 복잡한 보고서 계산처럼 CPU를 오래 쓰는 작업은 적절한 크기의 플랫폼 스레드 풀이나 작업 큐가 더 낫다. 또한 synchronized 블록 안에서 네트워크나 DB 호출을 오래 수행하면 다른 작업이 기다리며 처리량이 낮아진다. 공유 상태를 줄이고, 짧은 임계 영역만 잠그며, 블로킹 호출은 잠금 밖으로 분리하는 구조를 우선 검토한다.
관측도 필수다. 요청 지연 시간만 보지 말고 활성 요청 수, DB 풀 대기 시간, 외부 API별 timeout 수, JVM CPU 사용률, 오류 재시도 횟수를 함께 기록한다. 가상 스레드 도입 후 활성 스레드 수가 크게 늘어도 지연 시간과 자원 사용량이 개선되지 않는다면 병목은 스레드가 아니라 데이터베이스, 외부 서비스 또는 동기화 구간에 있을 가능성이 높다.
배포 체크리스트
- 실행 환경이 Java 21 이상인지 확인한다.
- I/O 중심 엔드포인트 한 곳에서 부하 테스트를 먼저 수행한다.
- HTTP timeout, DB 커넥션 풀, 외부 API 동시성 제한을 함께 설정한다.
- CPU 작업과 장시간 락 구간은 별도 실행 정책으로 분리한다.
- 배포 전후 p95 지연, 오류율, DB 대기, CPU 사용률을 같은 기준으로 비교한다.
가상 스레드는 요청 처리 코드를 읽기 쉬운 동기식 형태로 유지하면서 높은 I/O 동시성을 만들 수 있다. 다만 성패는 스레드 수가 아니라 외부 자원 제한, 취소와 timeout, 그리고 운영 지표를 함께 설계하는 데 달려 있다.
java
| No | 작성일 | Title |
|---|---|---|
| 2337 | 2026. 04. 06. | Java 24 Structured Concurrency 실전 가이드 - StructuredTaskScope로 안전한 동시성 구현 |
| 2322 | 2026. 04. 05. | Java 24 Compact Object Headers로 힙 메모리 25% 절약하기 |
| 2298 | 2026. 04. 05. | Java 24 Stream Gatherers 실전 활용법: 커스텀 중간 연산 만들기 |
| 2297 | 2026. 04. 05. | Java 25 Structured Concurrency 실전 가이드: StructuredTaskScope 완전 정복 |
| 2270 | 2026. 04. 04. | Java 21-23 패턴 매칭과 Record Patterns: switch문의 혁명적 진화 |
| 2269 | 2026. 04. 04. | Java 21 Virtual Threads 완벽 가이드: 기존 스레드 대비 100배 성능 향상 달성하기 |
| 2201 | 2026. 02. 11. | 모던 Java 빌드 도구 비교: Gradle vs Maven 2025 |
| 2200 | 2026. 02. 11. | Java Stream API 고급 패턴과 성능 최적화 |
| 2199 | 2026. 02. 11. | JVM 메모리 튜닝과 GC 최적화 완벽 가이드 |
| 2198 | 2026. 02. 11. | Java Record와 Sealed Class 실전 디자인 패턴 |