1. 어디에 쓰는 설계인가

이 설계는 아래 같은 서비스에 맞습니다.

  • 사내 업무 API
  • BFF
  • CRUD 중심 백엔드
  • 외부 API 호출이 많은 중계/집계 서비스
  • DB I/O, HTTP I/O 비중이 높은 시스템

반대로 CPU 연산이 주업무인 서비스는 Virtual Thread만으로 이득이 크지 않습니다. Virtual Thread는 “연산을 빠르게”가 아니라 대기 시간을 견디는 동시성 모델에 더 가깝기 때문입니다. Spring 쪽도 virtual threads가 많아지면 비동기 모델의 필요성이 줄어드는 많은 경우를 이야기하지만, 네트워크 지연 자체를 없애는 건 아니라고 설명합니다.


2. 권장 방향 한 줄 요약

Spring MVC + Tomcat + JPA/JDBC + RestClient + Virtual Thread
이 조합으로 가는 게 제일 현실적입니다.

Spring Boot 4는 Tomcat 11 / Servlet 6.1 기반이고, Jakarta EE 11 정렬이 이미 되어 있습니다. 즉 Boot 4에서는 굳이 “무조건 WebFlux”로 갈 이유가 더 줄었습니다.


3. 아키텍처 그림

[ Client ]
|
v
[ API Gateway / LB ]
|
v
[ Spring Boot 4 App ]
- Spring MVC Controller
- Service Layer
- Domain / UseCase
- Repository (JPA/JDBC)
- Outbound Client (RestClient / HTTP Interface Client)
|
+--> [ PostgreSQL / MySQL ]
+--> [ Redis ]
+--> [ External APIs ]
+--> [ Kafka / MQ ] (선별 적용)
 

실행 모델은 이렇게 보시면 됩니다.

요청 1건 = Virtual Thread 1개
Controller -> Service -> JPA/JDBC -> 외부 API 호출
블로킹 코드를 유지하되, 스레드 비용을 낮춰 동시성을 확보
 

4. 설계 원칙

원칙 1) “동기 코드 유지”를 기본값으로 둡니다

Virtual Thread의 가장 큰 장점은 복잡한 reactive 체인 없이도 높은 동시성 구조를 만들기 쉽다는 점입니다. 그래서 Boot 4에서는 먼저 동기 코드로 설계하고, 정말 필요한 곳만 별도 비동기/리액티브로 분리하는 게 낫습니다. Spring은 virtual threads를 활성화하면 기본 task executor와 scheduler가 virtual-thread 기반으로 바뀐다고 문서화하고 있습니다.


원칙 2) CPU-bound 작업은 분리합니다

이건 제 설계 판단입니다.
Virtual Thread는 I/O 대기 처리엔 좋지만, 이미지 변환, 대용량 압축, 암호화 일괄처리, 복잡한 통계 계산처럼 CPU를 오래 점유하는 작업은 별도 platform-thread 풀이나 배치 워커로 분리하는 게 맞습니다.

즉:

  • I/O-bound: Virtual Thread
  • CPU-bound: 고정 크기 platform thread pool
  • 대량 비동기 배치: 별도 worker / queue

원칙 3) DB 커넥션 풀은 그대로 “작게, 정확하게” 잡습니다

여기서 많이 착각합니다.

Virtual Thread를 쓴다고 DB 커넥션도 무한정 늘릴 수 있는 게 아닙니다.
실제 병목은 대부분 DB 커넥션 풀, DB lock, 외부 API rate limit입니다.

그래서:

  • Virtual Thread 수 ≠ DB 커넥션 수
  • HikariCP 풀 크기는 DB가 감당 가능한 수준으로 유지
  • 애플리케이션 동시성은 높여도, 외부 자원은 제한해야 함

Boot는 DataSource 계측과 Hikari 메트릭을 기본 지원합니다. 그래서 튜닝은 감으로 하지 말고 메트릭 보고 가야 합니다.


원칙 4) pinned virtual thread를 운영 지표로 봐야 합니다

Spring Boot 문서는 virtual thread 활성화 전에 Pinned Virtual Threads를 꼭 확인하라고 경고합니다. 감지 방법으로 JDK Flight Recorder와 jcmd도 안내합니다.

실무적으로 pinned가 자주 생기는 구간은 보통 이렇습니다.

  • 오래 잡는 synchronized
  • 오래 블로킹되는 네이티브 호출
  • 일부 라이브러리의 내부 락
  • 메시징/리스너 계열의 특정 동시성 패턴

Spring Kafka 문서도 virtual threads와 concurrent message listener 조합에서는 pinning 및 race condition 가능성을 주의하라고 명시합니다.


원칙 5) 스케줄러는 “virtual thread니까 무한정”으로 생각하면 안 됩니다

Virtual Thread가 켜지면 scheduler도 SimpleAsyncTaskScheduler 쪽으로 가고, pooling 관련 설정은 무시됩니다. 즉 기존 풀 크기 조절 감각과 다르게 봐야 합니다. 또 virtual thread는 daemon thread라서 스케줄러가 앱 생존을 보장하지 않으므로 spring.main.keep-alive=true를 같이 두는 쪽이 안전합니다.


5. 추천 기술 스택

르무엘님 스타일로 현실적인 조합 드리면:

  • Spring Boot 4
  • Spring MVC
  • Tomcat 11

데이터

  • Spring Data JPA 또는 JdbcClient/JdbcTemplate
  • HikariCP
  • PostgreSQL

외부 호출

  • RestClient 또는 HTTP Interface Client
  • Resilience4j 또는 Spring Cloud 쪽 circuit breaker는 선택

관측성

  • Actuator
  • Micrometer
  • Prometheus
  • Grafana
  • JFR

비동기

  • 기본은 Virtual Thread
  • CPU-heavy 전용 executor 따로

6. 패키지 구조 추천

com.example.order
├─ api
│ ├─ OrderController
│ └─ dto
├─ application
│ ├─ OrderService
│ ├─ OrderQueryService
│ └─ port
├─ domain
│ ├─ Order
│ ├─ OrderStatus
│ └─ policy
├─ infrastructure
│ ├─ persistence
│ ├─ client
│ ├─ config
│ └─ messaging
└─ support
├─ concurrency
├─ logging
└─ observability
 

포인트는 support.concurrency를 따로 두는 겁니다.
여기에 아래를 모읍니다.

  • virtual thread 관련 설정
  • CPU-bound executor
  • bulkhead / semaphore
  • timeout 정책

7. 설정 예시

application.yml

 
spring:
threads:
virtual:
enabled: true
main:
keep-alive: true

management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,threaddump
 

spring.threads.virtual.enabled=true와 spring.main.keep-alive=true는 Spring Boot 공식 문서의 virtual threads 섹션에 근거한 설정입니다.


CPU 작업 분리용 Executor

 
@Configuration
public class ExecutorConfig {

@Bean(name = "cpuBoundExecutor")
public ExecutorService cpuBoundExecutor() {
int n = Runtime.getRuntime().availableProcessors();
return Executors.newFixedThreadPool(Math.max(2, n));
}
}
 

그리고 서비스에서는 이렇게 분리합니다.

  • DB 조회 / 외부 API / 일반 요청 처리 → 기본 virtual thread 흐름
  • 엑셀 대량 생성 / 이미지 리사이징 / 대용량 해시 → cpuBoundExecutor

이건 Spring 공식 강제 규칙은 아니고, 운영 안정성을 위한 실무 설계입니다.


8. 서비스 레이어 설계 규칙

요청 처리

Controller
-> Application Service
-> Repository / External Client
 

여기서는 그냥 평범한 blocking 코드로 가시면 됩니다.

 
@Transactional
public OrderDetail getOrder(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
CustomerInfo customer = customerClient.getCustomer(order.getCustomerId());
return mapper.toDetail(order, customer);
}
 

이런 구조가 Virtual Thread에서 가장 이득 보기 좋습니다.
코드를 reactive 스타일로 비틀지 않아도 동시성 이점을 가져가기 쉽기 때문입니다. 이는 Spring의 virtual thread 방향성과도 맞습니다.


9. 운영상 꼭 넣어야 할 보호장치

1) 외부 API bulkhead / semaphore

Virtual Thread가 많다고 외부 API까지 무한 호출하면 바로 터집니다.
외부 파트너 API는 반드시 동시 호출 제한을 거세요.

예:

  • 결제 API 동시 50
  • 사내 레거시 API 동시 20
  • 파일 변환 동시 4

2) DB timeout / HTTP timeout

Virtual Thread는 대기 비용을 줄여주지만, 느린 자원을 빠르게 만들진 않습니다.
그래서 timeout이 더 중요합니다.

  • DB query timeout
  • RestClient connect/read timeout
  • transaction timeout

3) 메트릭

Boot는 HTTP 서버 메트릭, task executor/scheduler 메트릭, datasource/Hikari 메트릭, application startup 메트릭 등을 Actuator/Micrometer로 노출합니다.

최소한 이건 보셔야 합니다.

  • http.server.requests
  • jdbc.connections.*
  • hikaricp.*
  • application.ready.time
  • JVM thread / GC
  • pinned VT 관련 JFR 이벤트

10. 메시징/Kafka는 보수적으로

HTTP 요청-응답 API는 Virtual Thread와 궁합이 좋습니다.
그런데 Kafka listener처럼 내부 라이브러리 동시성 모델이 강한 쪽은 바로 “전면 virtual thread”로 가기보다 별도 검증이 필요합니다. Spring Kafka 문서가 virtual threads + concurrent listener에 주의를 주는 이유가 그겁니다.

제 권장안은 이겁니다.

  • Web/API 레이어: 먼저 virtual thread 적용
  • Batch / Kafka consumer / scheduler-heavy 구간: 별도 검증 후 단계 적용

11. 르무엘님용 최종 권장안

추천 아키텍처

Boot 4 + Java 21/24+ + MVC + Tomcat + JPA + RestClient + Virtual Thread

적용 우선순위

  1. 사내 API/BFF부터 적용
  2. 외부 API aggregation 서비스 적용
  3. DB-heavy CRUD 적용
  4. Kafka/Batch는 마지막

적용 금지 구간

  • CPU 소모 큰 파일 변환을 전부 virtual thread에 태우는 것
  • thread 수가 늘었으니 DB pool도 같이 크게 늘리는 것
  • scheduler daemon 이슈 무시하는 것
  • pinned VT 관측 없이 운영 투입하는 것

12. 한 줄 결론

Spring Boot 4 + Virtual Thread의 정석은 “동기 코드를 유지하면서 I/O 동시성을 높이고, CPU 작업과 외부 자원은 따로 제한하는 구조”입니다.
기술 포인트는 Virtual Thread 자체보다, DB/외부 API/메시징 병목을 분리해서 설계하는 것입니다. Spring Boot 4는 Java 17+와 Tomcat 11/Servlet 6.1 기반이며, virtual threads는 Java 21+에서 spring.threads.virtual.enabled=true로 켤 수 있고, Boot는 기본 executor/scheduler 동작과 keep-alive 주의사항까지 공식 문서로 안내하고 있습니다.

LIST

+ Recent posts