Virtual Thread 마스터 — Spring Boot 통합

2026-05-03확률과 통계 마스터 노트

Java 21 Virtual Thread 마스터 노트 시리즈 5편. Spring Boot 3.2+의 Virtual Thread 기본 활성화, Tomcat·Jetty·Undertow 모두 Virtual Thread Worker, @Async 자동 통합, ThreadPoolTaskExecutor 대체, Spring MVC vs WebFlux 선택의 결정적 변화, 기존 Spring 앱 마이그레이션 패턴까지.

이 글은 Java 21 Virtual Thread 마스터 노트 시리즈의 다섯 번째 편입니다. 4편(Pinning)에서 함정을 봤다면, 이번엔 Spring Boot 통합 — 한 줄 설정으로 시작.

Spring Boot 3.2+ = Virtual Thread 기본 지원. Tomcat·Jetty·Undertow·@Async 모두 자동 통합. 기존 Spring MVC 앱이 WebFlux 비슷한 처리량.

처음 Spring 통합이 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, "기존 Spring MVC를 WebFlux로 바꿔야 하나" 막연합니다. 둘째, @Async·ThreadPool과 어떻게 연동하나 막연합니다.

해결법은 한 가지예요. "Spring MVC + Virtual Thread = 처리량 ≈ WebFlux" 한 줄. WebFlux 전환 강제 X. 한 줄 설정으로 시작.

Spring Boot 3.2+ — 한 줄 설정

spring:
  threads:
    virtual:
      enabled: true

이 한 줄로:

  • Tomcat·Jetty·Undertow Worker → Virtual Thread
  • @Async 기본 → Virtual Thread Per Task
  • @Scheduled → Virtual Thread (스케줄러 자체는 Platform)
  • TaskExecutor Bean 자동 → Virtual Thread

여기서 정말 중요한 시험 함정 — Spring Boot 3.2 + Java 21이 표준. 둘 다 필요. 3.1 이하는 수동 설정.

Tomcat Virtual Thread Worker

spring:
  threads:
    virtual:
      enabled: true
이전 (Tomcat 기본):
  worker pool 200개 → 200 동시 요청
  
이후 (Virtual Thread):
  Virtual Thread per request → 수만 동시 요청

여기서 정말 중요한 시험 함정 — WebFlux 안 써도 됨. 기존 Spring MVC + Tomcat + Virtual Thread = WebFlux 비슷한 처리량. 코드 변경 X.

@Async 통합

@Service
public class NotificationService {
    
    @Async
    public CompletableFuture<Void> sendEmail(User user) {
        emailClient.send(user.getEmail(), "...");   // 블로킹 OK
        return CompletableFuture.completedFuture(null);
    }
}
spring:
  threads:
    virtual:
      enabled: true

자동으로 Virtual Thread 위에서 실행. 별도 풀 설정 X.

명시적 Executor Bean

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean
    public TaskExecutor taskExecutor() {
        return new VirtualThreadTaskExecutor("vt-async-");
    }
}

VirtualThreadTaskExecutor (Spring 6.1+).

@Scheduled

@Component
public class ReportScheduler {
    
    @Scheduled(fixedRate = 60_000)
    public void generateReport() {
        // 1분마다, Virtual Thread에서 실행
        reportService.generate();
    }
}

스케줄링 자체는 Platform Thread (SingleThreadScheduledExecutor), 작업은 Virtual Thread.

ThreadPoolTaskExecutor 대체

// 이전
@Bean
public TaskExecutor taskExecutor() {
    var executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(50);
    executor.setMaxPoolSize(200);
    executor.setQueueCapacity(1000);
    return executor;
}

// 이후
@Bean
public TaskExecutor taskExecutor() {
    return new VirtualThreadTaskExecutor("worker-");
}

여기서 정말 중요한 시험 함정 — Pool 크기 설정 의미 X (Virtual Thread). Per-Task 자동. 큐도 불필요.

Spring MVC vs WebFlux — 결정적 변화

측면 Spring MVC WebFlux MVC + Virtual Thread
프로그래밍 모델 동기·블로킹 Reactive 동기·블로킹
처리량 낮음 (Pool 한정) 매우 높음 매우 높음
학습 곡선 낮음 높음 낮음
디버깅 쉬움 어려움 쉬움
라이브러리 풍부 Reactive 한정 풍부

여기서 정말 중요한 시험 함정 — MVC + Virtual Thread = WebFlux 효율 + MVC 친화. 기존 코드 + 한 줄 설정. 새 프로젝트에선 WebFlux 강제 X.

WebFlux는 여전히 필요?

WebFlux 권장:
  - 백프레셔가 결정적 (대용량 스트리밍)
  - 함수형 합성 자연스러운 도메인
  - Reactive 라이브러리 풍부 (R2DBC·Reactive Mongo)
  
MVC + Virtual Thread:
  - 일반 CRUD·REST API
  - 기존 코드베이스
  - 학습 곡선 낮춤

Spring Data JDBC·JPA

spring:
  threads:
    virtual:
      enabled: true
  datasource:
    url: jdbc:postgresql://...
    driver-class-name: org.postgresql.Driver

기존 JDBC·JPA 그대로. Virtual Thread가 자동 효율.

여기서 시험 함정이 하나 있어요. JDBC 드라이버 = Pinning 점검. PostgreSQL 42.7+, MySQL 8.x+ 권장. 옛 드라이버 = synchronized 사용·Pinning 위험.

Connection Pool — HikariCP

spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10

여기서 정말 중요한 시험 함정 — Connection Pool 크기 ≠ Virtual Thread 수. Pool = DB가 견딜 수. VT는 풀 대기 자동. 대량 동시 = DB 부하 폭주 위험. 큐·Rate Limit 필요.

REST 클라이언트

RestTemplate (블로킹)

@Bean
public RestTemplate restTemplate() {
    return new RestTemplateBuilder().build();
}

// 사용
ResponseEntity<User> response = restTemplate.getForEntity(url, User.class);
// Virtual Thread 환경에서 unmount OK

WebClient (비동기·Reactive)

@Bean
public WebClient webClient() {
    return WebClient.create();
}

User user = webClient.get().uri(url)
    .retrieve()
    .bodyToMono(User.class)
    .block();   // Virtual Thread에서 OK

여기서 정말 중요한 시험 함정 — WebClient + .block()도 OK (Virtual Thread 환경). 이전엔 .block()이 Platform Thread 점유 = 안티패턴. 이제 Virtual Thread에선 효율적.

RestClient (Spring 6.1+)

@Bean
public RestClient restClient() {
    return RestClient.create();
}

// 동기 + 모던 API
User user = restClient.get().uri(url)
    .retrieve()
    .body(User.class);

WebClient의 동기 버전. Virtual Thread 친화.

Spring Test

@SpringBootTest(properties = "spring.threads.virtual.enabled=true")
class MyTest {
    @Test
    void test() {
        // Virtual Thread 환경에서 실행
    }
}

테스트도 동일. 별도 설정 X.

Logback·Spring Logger

Logback 1.4.x+ — Virtual Thread 친화 (synchronized 제거)

옛 버전 = Pinning 위험. 최신 권장.

마이그레이션 체크리스트

✓ Java 21 업그레이드
✓ Spring Boot 3.2+ 업그레이드
✓ spring.threads.virtual.enabled=true
✓ ThreadPoolTaskExecutor → VirtualThreadTaskExecutor
✓ JDBC 드라이버 최신 (PostgreSQL 42.7+ 등)
✓ HikariCP 5+
✓ Logback 1.4+
✓ 모든 의존성 Pinning 점검
✓ -Djdk.tracePinnedThreads=full (개발)
✓ 부하 테스트 (이전·이후 비교)

처리량 비교 (예시)

시나리오: 1초 I/O 대기하는 REST 엔드포인트, 1000 동시 요청

Spring MVC + Tomcat (기본 Pool 200):
  → 처리 시간 ~5초 (200 동시만)
  → 이후 200개씩 순차

Spring MVC + Virtual Thread:
  → 처리 시간 ~1초 (1000 동시)
  → WebFlux와 비슷

이렇게 큰 차이. 한 줄 설정으로.

운영 모니터링

Actuator

management:
  endpoints:
    web:
      exposure:
        include: metrics,threaddump
curl /actuator/threaddump
# Virtual Thread 정보 포함

Micrometer 메트릭

jvm.threads.virtual.live   # 현재 Virtual Thread 수
jvm.threads.virtual.peak   # 최대 도달

Spring Boot 3.2+ 자동 노출.

CompletableFuture + Virtual Thread

@Service
public class ParallelService {
    
    @Autowired
    private VirtualThreadTaskExecutor executor;
    
    public CompletableFuture<Combined> fetchAll() {
        var f1 = CompletableFuture.supplyAsync(() -> userClient.fetch(), executor);
        var f2 = CompletableFuture.supplyAsync(() -> orderClient.fetch(), executor);
        var f3 = CompletableFuture.supplyAsync(() -> productClient.fetch(), executor);
        
        return CompletableFuture.allOf(f1, f2, f3)
            .thenApply(v -> new Combined(f1.join(), f2.join(), f3.join()));
    }
}

병렬 호출 + 합성. Virtual Thread가 백엔드.

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 5편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • Spring Boot 3.2+ + Java 21 = 표준
  • spring.threads.virtual.enabled: true 한 줄
  • 자동 — Tomcat·Jetty·Undertow Worker·@Async·@Scheduled·TaskExecutor
  • WebFlux 강제 X — MVC + VT = 비슷한 처리량
  • @Async — 자동 Virtual Thread (또는 명시 VirtualThreadTaskExecutor)
  • @Scheduled — 스케줄러 Platform·작업 Virtual
  • ThreadPoolTaskExecutor → VirtualThreadTaskExecutor
  • Pool 크기 의미 X (Per-Task)
  • MVC + VT vs WebFlux — 학습·디버깅·라이브러리 풍부 vs 백프레셔
  • 일반 = MVC + VT / 백프레셔 결정적 = WebFlux
  • JDBC·JPA — 기존 그대로, 드라이버 최신 권장
  • Connection Pool ≠ VT 수 — DB 한계만큼
  • 대량 동시 = DB 부하 → 큐·Rate Limit
  • WebClient .block() OK (Virtual Thread 환경)
  • RestClient (Spring 6.1+) = 동기·모던
  • Logback 1.4+ Virtual Thread 친화
  • 마이그레이션 — Java 21·Spring Boot 3.2·드라이버·HikariCP·Logback·Pinning 점검
  • 메트릭 — jvm.threads.virtual.live·peak
  • CompletableFuture + executor 명시 = 병렬 합성

시리즈 다른 편

공식 문서: Spring Boot 3.2 Release Notes 에서 더 깊이.

다음 글(6편)에서는 Structured Concurrency — 여러 Virtual Thread를 단위로 관리, StructuredTaskScope, 자동 취소·실패 전파까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!