Virtual Thread 마스터 — Performance·JFR·메모리

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

Java 21 Virtual Thread 마스터 노트 시리즈 7편. Virtual Thread 성능 측정의 결정적 지표, JFR로 Pinning·블로킹·시간 분석, JMH 벤치마크 설정, 처리량·지연·메모리 비교 (Platform Thread vs Virtual Thread vs WebFlux), GC 영향과 힙 사용 패턴, 실전 측정 워크플로우까지.

이 글은 Java 21 Virtual Thread 마스터 노트 시리즈의 일곱 번째 편입니다. 1~6편이 기능이었다면, 이번엔 그것이 얼마나 빠르고 효율인가 — Performance.

Virtual Thread는 빠르다·효율적이다지만 측정 안 하면 모름. JFR로 Pinning 추적. JMH로 정확한 벤치마크. 메모리·GC 영향 평가.

처음 Performance가 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, 무엇을 측정해야 할지 막연합니다. 둘째, JFR·JMC·JMH 도구가 한 번에 등장합니다.

해결법은 한 가지예요. "3 지표" — 1) 처리량(TPS), 2) 지연(Latency p99), 3) 메모리. JFR로 추적, JMH로 비교, Profiler로 hotspot. 이 셋만 잡으면 끝.

핵심 지표 3

1. 처리량 (Throughput·TPS) — 초당 요청 수
2. 지연 (Latency) — 요청 처리 시간 (p50·p95·p99)
3. 메모리 (Memory) — Heap·Stack·Direct

추가 — GC 빈도·시간, CPU 사용률.

벤치마크 — 시나리오

시나리오 1: I/O Bound (HTTP 호출)
시나리오 2: I/O Bound (DB 쿼리)
시나리오 3: 혼합 (CPU + I/O)
시나리오 4: CPU Bound

각 시나리오에서 Platform Thread vs Virtual Thread vs WebFlux 비교.

I/O Bound 처리량 비교

1초 sleep 후 응답 (1만 요청)

Platform Thread (Pool 200):
  처리 시간: 50초 (200 동시만)
  TPS: ~200

Virtual Thread:
  처리 시간: ~1초 (모두 동시)
  TPS: ~10,000

WebFlux (Reactor):
  처리 시간: ~1초
  TPS: ~10,000

여기서 정말 중요한 시험 함정 — I/O Bound = Virtual Thread = WebFlux (처리량). 코드 단순함은 Virtual Thread 우월.

CPU Bound 비교

무거운 계산 1초 (1만 요청)

Platform Thread (Pool = CPU 코어 수):
  코어 수만큼 동시 = ~코어 처리량

Virtual Thread:
  같음 (CPU가 한정)
  + 컨텍스트 스위칭 약간 더 (캐리어 변경)
  → 미세하게 느릴 수 있음

여기서 시험 함정이 하나 있어요. CPU Bound = Virtual Thread 효과 X 또는 약간 손해. ForkJoinPool·Platform Thread 권장.

메모리 비교

1만 idle 스레드:

Platform Thread:
  ~20 GB (스레드당 2MB)
  
Virtual Thread:
  ~수십 MB (스레드당 ~수 KB)
  
1000배 효율

여기서 정말 중요한 시험 함정 — Virtual Thread 메모리는 Heap 안. GC 대상. 평가 시 GC 빈도·시간 함께 측정.

JFR — Java Flight Recorder

# 시작 시 활성화
java -XX:StartFlightRecording=duration=60s,filename=app.jfr MyApp

# 실행 중 시작
jcmd <pid> JFR.start duration=60s filename=app.jfr

# 분석 — JMC (Java Mission Control)
jmc app.jfr

JFR이 자동 추적:

  • 스레드 상태 (mounted·waiting·blocked)
  • Pinning 이벤트
  • I/O·GC·CPU 사용
  • 메서드 hotspot

Virtual Thread 이벤트

JFR 이벤트:
  - jdk.VirtualThreadStart
  - jdk.VirtualThreadEnd
  - jdk.VirtualThreadPinned
  - jdk.VirtualThreadSubmitFailed

특히 VirtualThreadPinned = Pinning 발견.

jfr print --events jdk.VirtualThreadPinned app.jfr

여기서 정말 중요한 시험 함정 — JFR이 Pinning 자동 추적. 운영 환경에서도 가벼움 (~1% 오버헤드). 항상 활성 권장.

JMH 벤치마크

dependencies {
    testImplementation 'org.openjdk.jmh:jmh-core:1.37'
    testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class VirtualThreadBenchmark {
    
    @Benchmark
    public void platformThread() throws Exception {
        try (var executor = Executors.newFixedThreadPool(200)) {
            for (int i = 0; i < 1000; i++) {
                executor.submit(() -> ioOperation());
            }
        }
    }
    
    @Benchmark
    public void virtualThread() throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 1000; i++) {
                executor.submit(() -> ioOperation());
            }
        }
    }
    
    private void ioOperation() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {}
    }
}

여기서 시험 함정이 하나 있어요. JMH는 정확한 벤치마크 표준. 단순 timer는 JIT·warmup 등 변수 무시. 작은 차이 = JMH 필수.

async-profiler

# 다운로드
wget https://github.com/async-profiler/async-profiler/releases/...

# CPU 프로파일
./profiler.sh -d 60 -f profile.html <pid>

CPU·Wall·Allocation·Lock 프로파일. Flamegraph 자동.

처리량 측정 — wrk·Gatling

# wrk
wrk -t12 -c1000 -d30s http://localhost:8080/api

# 결과
Running 30s test
  12 threads and 1000 connections
  Thread Stats   Avg      Stdev    Max   +/- Stdev
    Latency    50.5ms    20.3ms   500ms   80%
    Req/Sec     1.5k     0.3k     2.0k    70%
  450,000 requests in 30s, 50MB read
Requests/sec:  15000.0

부하 테스트 도구. -c (동시 연결) 늘려 한계 측정.

Spring Boot Actuator — 메트릭

management:
  endpoints:
    web:
      exposure:
        include: prometheus,metrics

management:
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
curl /actuator/prometheus | grep http_server_requests

p50·p95·p99 자동 노출.

Pinning vs Non-Pinning 처리량

1000 동시 + 1초 sleep:

Pinning 없음:
  ~1초 처리, TPS ~1000

Pinning 30%:
  ~3초 처리, TPS ~300

Pinning 100% (모두 synchronized):
  Platform Thread Pool과 같음, TPS ~코어 수

여기서 정말 중요한 시험 함정 — Pinning 비율 = 직접적 처리량 영향. 30% Pinning만 있어도 큰 손실. JFR로 추적 + 수정.

GC 영향

G1·ZGC·Shenandoah 비교 (Virtual Thread 환경)

G1: 일반 처리, 잠깐 pause
ZGC: 매우 짧은 pause (10ms 이하)
Shenandoah: 비슷

여기서 시험 함정이 하나 있어요. 수백만 VT = Heap 사용 ↑ = GC 빈도 ↑. ZGC·Shenandoah 권장 (낮은 지연). G1도 OK.

# JVM 옵션
-XX:+UseZGC
-Xmx16g
-Xms16g

메모리 모니터링

# Heap 분석
jcmd <pid> GC.heap_info

# 스레드 메모리
jcmd <pid> Thread.print
Virtual Thread 수 — 메트릭으로
jvm.threads.virtual.live
jvm.threads.virtual.peak

부하 시 동작 비교

부하 증가 시:

Platform Thread (Pool):
  Pool 가득 → 큐 대기 → 응답 지연 폭발
  
Virtual Thread:
  무한 생성 가능 (메모리 한도 안에서)
  → DB 풀·외부 API가 병목 됨
  → Backpressure 필요 (Rate Limit)

여기서 정말 중요한 시험 함정 — Virtual Thread = 무한 동시 X. DB·외부 API 한계가 새 병목. Rate Limit·Circuit Breaker 필수.

DB 풀 크기 결정

공식 (Hikari):
  연결 = (코어 수 * 2) + 디스크 spindles
  보통 10~30개 충분
  
Virtual Thread 환경:
  같은 공식
  연결 못 얻은 VT는 자동 대기 (unmount)

모니터링 체크리스트

✓ JFR 활성 (운영도 OK)
✓ jdk.VirtualThreadPinned 모니터링
✓ Spring Boot Actuator + Prometheus
✓ jvm.threads.virtual.live·peak
✓ DB Connection Pool 사용률
✓ Circuit Breaker (Resilience4j)
✓ Rate Limit (외부 API)
✓ wrk·Gatling 부하 테스트
✓ async-profiler hotspot
✓ GC 메트릭 (빈도·시간)
✓ ZGC 또는 Shenandoah 평가

실전 측정 워크플로우

1. JMH 벤치마크 — 마이크로 비교
2. wrk·Gatling — 통합 부하 테스트
3. JFR 60초 캡처 — 운영 워크로드
4. JMC 분석 — Pinning·hotspot
5. async-profiler — Flamegraph
6. Prometheus·Grafana — 시계열
7. 부하 증가 시 동작 (Rate Limit 작동?)

결론 — Virtual Thread 효과

측면 I/O Bound CPU Bound
처리량 수십~수백배 비슷
메모리 수백배 비슷
코드 복잡도 단순 비슷
GC 약간 ↑ 비슷

I/O 위주 마이크로서비스 = Virtual Thread 도입 가치 큼.

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

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

  • 핵심 지표 3 — 처리량(TPS)·지연(p99)·메모리
  • I/O Bound — VT = WebFlux 처리량 (코드 단순함은 VT 우월)
  • CPU Bound — VT 효과 X (Platform Thread·ForkJoinPool 권장)
  • 메모리 — VT 1000배 효율 (~수 KB vs 2MB)
  • JFR = 자동 추적, 운영도 OK (~1% 오버헤드)
  • 이벤트 — VirtualThreadStart·End·Pinned·SubmitFailed
  • VirtualThreadPinned = Pinning 자동 발견
  • JMC = JFR 분석 GUI
  • JMH = 마이크로 벤치마크 표준
  • async-profiler = CPU·Wall·Allocation·Lock 프로파일·Flamegraph
  • wrk·Gatling = 부하 테스트
  • Spring Boot Actuator — http.server.requests 히스토그램
  • p50·p95·p99 자동
  • Pinning 비율 = 직접적 처리량 영향
  • 30%만 있어도 큰 손실
  • GC — 수백만 VT = Heap 사용 ↑
  • ZGC·Shenandoah 권장 (낮은 pause)
  • VT = 무한 동시 X — DB·외부 API 한계
  • Rate Limit·Circuit Breaker 필수
  • DB Pool 크기 = 공식 그대로 (10~30)
  • VT는 풀 자동 대기 (unmount)
  • 운영 — JFR + Prometheus + Profiler 결합
  • 측정 워크플로우 — JMH → wrk → JFR → JMC → Profiler → Prometheus

시리즈 다른 편

공식 문서: Java Flight Recorder / JMH / async-profiler 에서 더 깊이.

다음 글(8편, 마지막)에서는 Patterns — Fan-out·Race·Pipeline·Producer-Consumer·Saga·실전 안티패턴까지 시리즈 마무리.

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

답글 남기기

error: Content is protected !!