본문으로 건너뛰기
김신건의 로그

[Spring] Virtual Threads (Project Loom): Spring 6.1+

· 수정 · 📖 약 3분 · 1,073자/단어 #spring #java #virtual-thread #loom #concurrency #performance
Virtual Threads, Project Loom, spring loom, Virtual Thread Executor, spring 가상 스레드, loom integration

정의

Virtual Thread 는 JDK 21+ 에서 GA 된 경량 thread. JVM 이 OS thread (carrier) 위에 가상 thread 를 M:N 매핑. 수백만 개 생성 가능 (OS thread 는 보통 수천 개 한계).

Spring 6.1+ (Spring Boot 3.2+) 가 virtual thread 를 1차 시민으로 지원. spring.threads.virtual.enabled=true 한 줄로 활성화.

기존 reactive (WebFlux, Reactor) 가 callback / chain 으로 동시성을 얻었다면, virtual thread 는 기존 명령형 코드 그대로 두고 thread 비용만 줄여 줌. blocking 호출이 OS thread 를 점유하지 않음.

활성화

spring:
  threads:
    virtual:
      enabled: true

이 한 줄이 영향:

  • Tomcat / Jetty 의 request thread pool → virtual thread
  • @Async 메서드 → virtual thread (별도 Executor 미지정 시)
  • @Scheduled → virtual thread
  • ApplicationTaskExecutor → virtual thread
  • WebClient blocking 호출도 OS thread 안 잡음

JDK 21+ 필수. Spring Boot 3.2+ 필수.

명령형 + 고성능: 가능?

기존 패러다임:

동시성 모델코드 스타일동시성 비용
OS thread + thread pool명령형 (try/catch, blocking call)OS thread 비싸 수천 한계
Reactive (WebFlux)비명령형 (Mono / Flux chain)thread 적게 쓰지만 코드 복잡
Virtual thread명령형 (기존 코드 그대로)thread 사실상 무료 (수백만 개)

virtual thread = “blocking 코드의 부활”. 코드 가독성 + reactive 수준 동시성.

직접 사용

// JDK 21 API
Thread.startVirtualThread(() -> {
    System.out.println("running on virtual thread");
});

// Executor
ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor();
exec.submit(() -> doWork());

// Builder
Thread.ofVirtual().name("worker-").start(() -> ...);

Spring 통합

Tomcat / Jetty

spring.threads.virtual.enabled=true → Tomcat 의 worker pool 이 virtual thread 사용. 요청마다 새 thread 생성 (pool 개념 사실상 불필요).

기존:

Tomcat thread pool = 200
요청 201개 동시 도착 → 1개 대기

virtual thread:

요청마다 새 virtual thread
수천 동시 요청도 OK (OS thread 는 carrier 몇 개만 공유)

@Async

@Configuration
@EnableAsync
class AsyncConfig {
    // spring.threads.virtual.enabled=true 면 자동으로 virtual thread Executor
}

@Service
class NotificationService {
    @Async
    public void sendEmail(String to) {
        // virtual thread 에서 실행
        smtpClient.send(to);   // blocking OK
    }
}

별도 Executor Bean 등록 안 하면 Spring 이 virtual thread Executor 자동 등록.

@Scheduled

@Scheduled(fixedRate = 1000)
public void cleanup() {
    // virtual thread 에서 실행
    // 무거운 작업도 carrier thread 안 잡음
}

TaskExecutor / TaskScheduler

@Bean
TaskExecutor taskExecutor() {
    return new VirtualThreadTaskExecutor("my-virtual-");
}

@Bean
TaskScheduler taskScheduler() {
    return new SimpleAsyncTaskScheduler();   // virtual thread 사용
}

함정 1: synchronized 블록 = pinning

virtual thread 가 synchronized 블록 안에서 blocking I/O 하면 carrier thread 에 pin = 다른 virtual thread 가 그 carrier 못 씀.

synchronized (lock) {
    db.query("...");   // ❌ pinning. carrier 점유.
}

해결:

  • synchronizedReentrantLock (LockSupport.park 기반, pinning 안 됨)
  • 또는 critical section 짧게 유지

JDK 24+ 에선 synchronized 도 pinning 안 되도록 개선 (JEP 491).

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    db.query("...");   // OK, virtual thread 가 carrier 양보
} finally {
    lock.unlock();
}

함정 2: ThreadLocal

virtual thread 마다 별도 ThreadLocalMap. 수백만 virtual thread + 큰 ThreadLocal = 메모리 폭탄.

// ❌
static final ThreadLocal<byte[]> CACHE = ThreadLocal.withInitial(() -> new byte[1_000_000]);
// 1M virtual thread = 1TB

해결:

  • JDK 21+ ScopedValue 사용: 불변, 블록 스코프 한정, virtual thread 친화적
  • 또는 ThreadLocal 남용 줄이기

java-threadlocal 참고.

함정 3: thread pool 의 의미 변화

기존 OS thread 풀의 목적:

  1. 재사용 (thread 생성 비용 절약)
  2. 동시성 제한 (rate limiting)

virtual thread:

  1. ❌ 재사용 불필요 (생성 비용 무시 가능)
  2. ✓ 동시성 제한은 여전히 필요 (DB 연결 풀, 외부 API rate limit)
// virtual thread + semaphore 로 동시성 제한
Semaphore dbLimit = new Semaphore(20);

public User findById(Long id) {
    dbLimit.acquire();
    try {
        return jdbcTemplate.queryForObject(...);
    } finally {
        dbLimit.release();
    }
}

또는 DataSource 의 maximum-pool-size 가 자연스러운 한계.

WebFlux vs Virtual Thread

선택 기준
├─ 코드 가독성 우선 + JDK 21 가능 → Virtual Thread
├─ 매우 높은 동시성 (10K+ concurrent) + 메모리 critical → WebFlux
├─ 외부 reactive API (R2DBC, reactive Kafka) 활용 → WebFlux
└─ 단순 마이크로서비스 + blocking DB → Virtual Thread 권장

reactive 가 단점은 코드 복잡도 + debugging 어려움. virtual thread 는 단점이 거의 없음 (synchronized pinning + ThreadLocal 메모리만 주의).

성능 비교 (대략적)

시나리오OS thread pool (200)Virtual thread
동시 요청 100, 응답시간 짧음동일동일
동시 요청 1000, 응답시간 1초80% 가 대기모두 즉시 처리
동시 요청 10000대부분 timeout처리 가능 (다른 자원이 병목 안 되면)
CPU bound 작업OS thread 가 빠름virtual 도 비슷, but 미세 손해

CPU bound 는 virtual thread 가 의미 없음. I/O bound 에서 큰 이득.

모니터링

management:
  endpoints:
    web:
      exposure:
        include: threaddump

/actuator/threaddump 가 virtual thread 도 포함. JDK 21+ 의 jcmd <pid> Thread.dump_to_file 도 활용.

Micrometer 가 metric 발행: jvm.threads.virtual.thread.count.

사용 사례

적합:

  • 동시 다발적 외부 API 호출 (HTTP, gRPC)
  • DB blocking call 많은 서비스
  • 백그라운드 작업 (이메일 발송, 알림)
  • WebSocket / SSE 연결 다수

부적합:

  • CPU 집약적 계산 (이미지 처리, 압축)
  • thread 자체가 자원인 경우 (특수 driver)

호환성

라이브러리 / 도구virtual thread 호환
JDBC (HikariCP, JDBC driver)OK (synchronized 부분 있음)
JPA / HibernateOK
Spring MVCOK
Spring SecurityOK
Tomcat / JettySpring Boot 3.2+ 에서 자동
RestClient / WebClient (blocking 모드)OK
synchronized 사용 라이브러리주의 (pinning)
Project Reactor호환은 되지만 의미 없음

대부분 호환. 일부 native code 또는 synchronized 무거운 라이브러리만 주의.

함정과 베스트 프랙티스

  • JDK 21+ 필수: 미만 버전은 virtual thread 자체 없음
  • spring.threads.virtual.enabled=true 한 줄로 시작
  • synchronizedReentrantLock 마이그레이션 검토: pinning 회피
  • ThreadLocal 남용 자제, ScopedValue 우선 검토
  • CPU bound 작업엔 virtual thread 의미 없음: 기존 ForkJoinPool 사용
  • 모니터링: thread count 증가 → carrier 부족 / pinning 의심
  • 테스트: 동시성 high 시나리오 테스트 필수 (e.g., 1000 동시 요청)
  • 호환성 점검: 큰 의존 라이브러리는 release note 확인

관련 위키

이 글의 용어 (6개)
[Java] ThreadLocaljava
정의 는 각 스레드마다 독립된 변수 사본을 가지게 해 주는 컨테이너. 로 접근하며, 한 스레드의 값은 다른 스레드에서 볼 수 없다. 쓰임은 두 가지: (1) 같은 인스턴스를 여러…
[Spring] @Scheduled와 @Asyncspring
정의 Spring은 @Scheduled (정기 작업)와 @Async (비동기 메서드)를 어노테이션으로 제공. ThreadPool, TaskScheduler 설정 가능. Quart…
[Spring] RequestContextHolder, LocaleContextHolder: ThreadLocal 기반 요청 컨텍스트spring
정의 Spring 은 HTTP 요청 / 응답 / locale / 트랜잭션 / 인증 같은 요청 범위 데이터를 ThreadLocal 에 저장 해 전역 노출한다. 메서드 인자로 일일이…
[Spring] Spring MVC: @RestController, @RequestMappingspring
정의 Spring MVC는 Servlet 기반 동기 웹 프레임워크. 이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. We…
[Spring] WebFlux: Reactive, Mono, Fluxspring
정의 Spring WebFlux는 Project Reactor 기반의 비동기·non-blocking 웹 프레임워크. Spring MVC와 별개. Netty 기본 (Tomcat도 …
Non-Blocking (논블로킹)concurrency
정의 Non-Blocking 은 호출이 결과를 기다리지 않고 즉시 반환 하는 실행 방식. 결과가 준비되지 않았다면 "아직 안 됐다" 라는 신호 ( , , , 등) 를 반환하고, …

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기