Spring Boot 3.4 Virtual Threads: 면접 질문과 성능 벤치마크

Spring Boot 3.4와 함께 Java 21 Virtual Threads를 마스터하세요. 15가지 면접 질문, 성능 벤치마크, 마이그레이션 패턴으로 기술 면접을 통과하세요.

Spring Boot 3.4 Virtual Threads: 면접 질문과 성능 벤치마크

Virtual Threads는 Java 21의 가장 중요한 진보 중 하나이며, Spring Boot 3.4는 이를 네이티브로 통합합니다. Project Loom의 이 기능은 백엔드 애플리케이션에서 동시성을 다루는 방식을 근본적으로 바꿉니다. 기술 면접에서는 내부 메커니즘에 대한 이해, 적절한 사용 사례, 흔히 빠지는 함정에 대한 인지가 평가 대상이 됩니다.

준비 팁

면접관은 Virtual Threads를 진정으로 이해하는 지원자와 무작정 사용하는 지원자를 구분합니다. 사용해서는 안 되는 시점을 아는 것이 장점을 아는 것만큼 중요합니다.

Virtual Threads의 기초

질문 1: Virtual Thread란 무엇이며 Platform Thread와 어떻게 다른가

Virtual Thread는 운영체제가 아닌 JVM이 관리하는 경량 스레드입니다. Platform Threads(전통적인 스레드)와 달리 Virtual Threads는 OS 스레드에 직접 매핑되지 않습니다. JVM은 매우 적은 메모리만으로 수백만 개의 Virtual Thread를 만들 수 있습니다.

VirtualThreadDemo.javajava
// Comparing thread creation approaches
public class VirtualThreadDemo {

    public void demonstrateDifference() {
        // Platform Thread: ~1MB stack per thread
        // Practical limit: a few thousand on a standard JVM
        Thread platformThread = new Thread(() -> {
            performBlockingOperation();
        });

        // Virtual Thread: ~a few KB per thread
        // Can create millions without issues
        Thread virtualThread = Thread.ofVirtual().start(() -> {
            performBlockingOperation();
        });
    }

    // A Virtual Thread "mounts" onto a Platform Thread (carrier)
    // During I/O blocking, it releases the carrier for other Virtual Threads
    private void performBlockingOperation() {
        try {
            Thread.sleep(1000); // Virtual Thread detaches from carrier here
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

근본적인 차이는 블로킹 시의 동작에 있습니다. Platform Thread가 블로킹되면(I/O, sleep) OS 스레드에 그대로 묶여 있습니다. 반면 Virtual Thread는 캐리어 스레드에서 unmount되어 다른 Virtual Threads가 그 캐리어를 사용할 수 있게 합니다.

질문 2: Spring Boot 3.4에서 Virtual Threads를 어떻게 활성화하는가

Spring Boot 3.4는 Virtual Threads 활성화를 단일 설정 프로퍼티로 단순화합니다. 프레임워크 전체가 자동으로 적응하며 Tomcat, REST 컨트롤러, 블로킹 호출 모두가 즉시 이 최적화의 혜택을 받습니다.

yaml
# application.yml
# Global Virtual Threads activation
spring:
  threads:
    virtual:
      enabled: true

# Optional Tomcat pool configuration
server:
  tomcat:
    threads:
      max: 200  # Less critical with Virtual Threads
      min-spare: 10
WebConfig.javajava
// Programmatic activation if needed
@Configuration
public class WebConfig {

    @Bean
    public TomcatProtocolHandlerCustomizer<?> virtualThreadCustomizer() {
        // Each HTTP request runs on a Virtual Thread
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

활성화는 Tomcat의 동작을 바꿉니다. 고정 크기 스레드 풀 대신 요청마다 자체 Virtual Thread를 받게 됩니다. 이 접근 방식은 전통적인 스레드 풀의 병목을 제거합니다.

질문 3: "mounting"과 "unmounting" 개념을 설명하십시오

Mounting은 Virtual Thread를 캐리어 스레드(Platform Thread)에 연결하는 동작을 의미합니다. Unmounting은 블로킹 작업 중 발생하며 캐리어를 다른 Virtual Threads에 풀어줍니다. 이 메커니즘은 CPU 자원을 최적으로 활용하게 해줍니다.

MountingDemo.javajava
// Illustrating the mounting/unmounting cycle
public class MountingDemo {

    public void demonstrateMounting() {
        Thread.ofVirtual().start(() -> {
            // MOUNTED: Virtual Thread is using a carrier thread
            System.out.println("Carrier: " + getCurrentCarrier());

            // UNMOUNTING: releases carrier during blocking
            performDatabaseQuery(); // Blocking JDBC call

            // REMOUNTED: may be on a different carrier
            System.out.println("New carrier: " + getCurrentCarrier());
        });
    }

    // Blocking operations trigger unmounting automatically
    private void performDatabaseQuery() {
        // JDBC connection, file read, network call...
        // Virtual Thread detaches during I/O wait
    }

    private String getCurrentCarrier() {
        // Gets the current carrier thread name
        return Thread.currentThread().toString();
    }
}

이 메커니즘은 개발자에게는 투명합니다. 코드는 전통적인 명령형 스타일로 작성되지만 JVM이 자동으로 캐리어 사용을 최적화합니다. 적은 수의 캐리어로 구성된 풀이 수백만 개의 Virtual Threads를 처리할 수 있습니다.

기술 노트

캐리어 스레드 수는 기본적으로 CPU 코어 수와 일치합니다. JVM은 ForkJoinPool을 통해 이 풀을 동적으로 조정합니다.

사용 사례와 안티패턴

질문 4: Virtual Threads는 언제 성능 향상을 가져다주는가

Virtual Threads는 외부 REST 호출, 데이터베이스 쿼리, 파일 읽기와 같은 I/O 바운드 작업에서 빛을 발합니다. 이러한 작업은 대부분의 시간을 대기에 사용하며 그 동안 Virtual Thread가 자신의 캐리어를 해제합니다.

IOBoundService.javajava
// Ideal case for Virtual Threads
@Service
public class IOBoundService {

    private final RestClient restClient;
    private final UserRepository userRepository;

    // Each call involves network and database waiting
    // Virtual Threads shine here
    public UserProfile enrichUserProfile(Long userId) {
        // DB call - VT detaches during SQL query
        User user = userRepository.findById(userId).orElseThrow();

        // External REST call - VT detaches during HTTP wait
        ExternalData externalData = restClient
            .get()
            .uri("/api/external/{id}", userId)
            .retrieve()
            .body(ExternalData.class);

        // Data aggregation
        return new UserProfile(user, externalData);
    }
}

이점은 훨씬 더 많은 동시 요청을 처리할 수 있다는 데서 옵니다. Platform Threads 200개와 100ms 요청에서 최대 처리량은 2,000 req/s입니다. Virtual Threads로는 동일한 머신에서 50,000 req/s 이상에 도달할 수 있습니다.

질문 5: Virtual Threads에서 피해야 할 안티패턴은 무엇인가

Virtual Threads는 CPU 바운드 작업이나 "pinning"을 유발하는 상황에는 적합하지 않습니다. Pinning은 블로킹에도 불구하고 Virtual Thread가 캐리어에 묶여 있어 가상화의 이점을 잃게 만드는 현상입니다.

AntiPatterns.javajava
// Examples of cases to avoid
@Service
public class AntiPatterns {

    // ANTI-PATTERN 1: CPU-intensive computation
    // Virtual Threads provide no benefit here
    public BigInteger computeFactorial(int n) {
        // 100% CPU, no I/O, no unmounting possible
        BigInteger result = BigInteger.ONE;
        for (int i = 2; i <= n; i++) {
            result = result.multiply(BigInteger.valueOf(i));
        }
        return result; // Carrier is monopolized throughout
    }

    // ANTI-PATTERN 2: Synchronized causes pinning
    private final Object lock = new Object();

    public void pinnedOperation() {
        synchronized (lock) { // PINNING: VT stays on carrier
            performDatabaseQuery(); // Unmounting doesn't happen!
        }
    }

    // SOLUTION: Use ReentrantLock
    private final ReentrantLock reentrantLock = new ReentrantLock();

    public void unpinnedOperation() {
        reentrantLock.lock();
        try {
            performDatabaseQuery(); // Unmounting possible
        } finally {
            reentrantLock.unlock();
        }
    }
}

자원 측면에서 pinning은 Virtual Thread를 사실상 Platform Thread로 만들어 버립니다. 주된 원인은 synchronized 블록과 JNI를 통한 네이티브 호출입니다. ReentrantLock으로 마이그레이션하면 첫 번째 경우를 해결할 수 있습니다.

질문 6: 애플리케이션에서 pinning을 어떻게 탐지하는가

JVM은 pinning 사례를 식별할 수 있는 진단 옵션을 제공합니다. 이 정보는 Virtual Threads로 마이그레이션할 때 매우 중요합니다. pinning은 성능을 개선하기는커녕 떨어뜨릴 수 있기 때문입니다.

PinningDiagnostics.javajava
// Configuration and pinning detection
public class PinningDiagnostics {

    // JVM option to log pinning
    // -Djdk.tracePinnedThreads=full (detailed)
    // -Djdk.tracePinnedThreads=short (summary)

    // Example code causing pinning
    public void demonstratePinning() {
        Thread.ofVirtual().start(() -> {
            synchronized (this) {
                // This log will appear with tracePinnedThreads enabled
                try {
                    Thread.sleep(100); // Pinned during sleep!
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
    }
}
bash
# Typical output with -Djdk.tracePinnedThreads=full
Thread[#23,VirtualThread[#1]/runnable@ForkJoinPool-1-worker-1,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
    java.base/java.lang.VirtualThread.parkOnCarrierThread(VirtualThread.java:661)
    com.example.PinningDiagnostics.demonstratePinning(PinningDiagnostics.java:15)

pinning 로그를 분석하면 수정해야 할 핫스팟이 드러납니다. pinning이 빈번한 애플리케이션은 Virtual Threads의 장점을 충분히 살리지 못하며, 오히려 Platform Threads보다 느려질 수도 있습니다.

Spring Boot 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

성능 벤치마크

질문 7: Virtual Threads로 어느 정도의 성능 향상을 기대할 수 있는가

벤치마크에 따르면 일반적인 I/O 바운드 애플리케이션에서 상당한 개선이 나타납니다. 향상 폭은 I/O 시간 대 CPU 시간의 비율과 요구되는 동시성 수준에 달려 있습니다.

BenchmarkController.javajava
// Endpoint for measuring performance
@RestController
@RequestMapping("/api/benchmark")
public class BenchmarkController {

    private final ExternalApiClient apiClient;

    // Simulation of a typical endpoint with external calls
    @GetMapping("/user/{id}")
    public ResponseEntity<UserData> getUser(@PathVariable Long id) {
        // Three sequential I/O calls
        UserInfo info = apiClient.fetchUserInfo(id);        // ~50ms
        List<Order> orders = apiClient.fetchOrders(id);     // ~80ms
        CreditScore score = apiClient.fetchCreditScore(id); // ~100ms

        return ResponseEntity.ok(new UserData(info, orders, score));
    }
}
text
# Benchmark results - 10,000 concurrent requests
# Configuration: 8 cores, 16GB RAM, simulated latency 230ms/request

Platform Threads (pool 200):
- Throughput: 850 req/s
- P99 latency: 1250ms
- Heap memory: 2.1GB

Virtual Threads:
- Throughput: 4200 req/s
- P99 latency: 280ms
- Heap memory: 850MB

Gain: 5x throughput, 4.5x P99 latency reduction

Virtual Threads는 메모리 사용량도 줄여줍니다. 각 스레드가 약 1MB가 아니라 수 KB만 차지하기 때문입니다.

질문 8: Virtual Threads에서 커넥션 풀은 어떻게 설정해야 하는가

커넥션 풀(HikariCP, Lettuce)은 Virtual Threads에서 새로운 병목이 됩니다. 10개 커넥션의 풀은 Virtual Thread가 수백만 개라도 동시 DB 쿼리를 10개로 제한합니다.

yaml
# application.yml
# HikariCP configuration optimized for Virtual Threads
spring:
  datasource:
    hikari:
      # More connections since Virtual Threads allow more concurrency
      maximum-pool-size: 50
      minimum-idle: 10
      # Shorter timeout due to more concurrent requests
      connection-timeout: 5000
      # Fast validation
      validation-timeout: 3000

  # Redis with Lettuce - already async-friendly
  data:
    redis:
      lettuce:
        pool:
          max-active: 50
          max-idle: 20
ConnectionPoolMonitor.javajava
// Pool monitoring to avoid contention
@Component
public class ConnectionPoolMonitor {

    private final HikariDataSource dataSource;

    @Scheduled(fixedRate = 10000)
    public void logPoolStats() {
        HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
        log.info("Pool stats - Active: {}, Idle: {}, Waiting: {}",
            pool.getActiveConnections(),
            pool.getIdleConnections(),
            pool.getThreadsAwaitingConnection());

        // Alert if too many threads waiting
        if (pool.getThreadsAwaitingConnection() > 100) {
            log.warn("Connection pool contention detected!");
        }
    }
}

풀 크기는 Virtual Thread 수가 아닌 데이터베이스의 처리 능력에 따라 결정됩니다. 표준 PostgreSQL은 약 100~200개의 활성 커넥션을 처리할 수 있습니다.

질문 9: Micrometer로 Virtual Threads의 영향을 어떻게 측정하는가

Micrometer와 Spring Boot Actuator는 Virtual Threads의 효과를 평가하기 위한 핵심 메트릭을 제공합니다. 이러한 메트릭을 통해 효과를 입증하고 잠재적 문제를 드러낼 수 있습니다.

VirtualThreadMetrics.javajava
// Custom metrics for Virtual Threads
@Component
public class VirtualThreadMetrics {

    private final MeterRegistry registry;
    private final AtomicLong activeVirtualThreads = new AtomicLong(0);

    @PostConstruct
    public void registerMetrics() {
        // Active Virtual Threads counter
        Gauge.builder("virtual.threads.active", activeVirtualThreads, AtomicLong::get)
            .description("Number of active virtual threads")
            .register(registry);

        // JVM metrics for carriers
        Gauge.builder("virtual.threads.carriers", this::getCarrierCount)
            .description("Number of carrier threads")
            .register(registry);
    }

    private double getCarrierCount() {
        // Gets the ForkJoinPool carrier count
        return ForkJoinPool.commonPool().getPoolSize();
    }

    // Interceptor to trace requests
    public void trackVirtualThread(Runnable task) {
        activeVirtualThreads.incrementAndGet();
        try {
            task.run();
        } finally {
            activeVirtualThreads.decrementAndGet();
        }
    }
}
yaml
# application.yml
# Metrics exposure
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

메트릭 분석을 통해 Virtual Threads 대 Carriers의 비율을 확인하고 커넥션 풀에서 경합이 발생한 시점을 식별할 수 있습니다.

마이그레이션과 호환성

질문 10: Virtual Threads와 호환되는 라이브러리는 무엇인가

호환성은 synchronized 블록 사용과 네이티브 호출에 좌우됩니다. Spring 생태계는 대체로 호환되지만 일부 라이브러리는 특정 버전을 요구합니다.

CompatibilityCheck.javajava
// Checking dependency compatibility
@Configuration
public class CompatibilityCheck {

    // Compatible libraries (recommended versions)
    // - Spring Boot 3.2+ (native support)
    // - HikariCP 5.1+ (ReentrantLock instead of synchronized)
    // - Lettuce 6.3+ (non-blocking I/O)
    // - Jackson 2.16+ (no synchronized)

    // Libraries requiring attention
    // - JDBC drivers: check version
    // - Some legacy HTTP clients

    @Bean
    public CommandLineRunner checkCompatibility() {
        return args -> {
            // Log versions for audit
            log.info("Java version: {}", System.getProperty("java.version"));
            log.info("Virtual threads available: {}",
                Thread.ofVirtual() != null);

            // Support verification
            if (Runtime.version().feature() < 21) {
                throw new IllegalStateException(
                    "Java 21+ required for Virtual Threads");
            }
        };
    }
}

대다수의 모던 프레임워크는 이미 ReentrantLock으로 전환했습니다. 레거시 의존성은 -Djdk.tracePinnedThreads=short를 활성화한 부하 테스트로 pinning 문제를 드러낼 수 있습니다.

질문 11: Virtual Threads로 점진적으로 마이그레이션하는 방법

점진적 마이그레이션은 운영 환경에 위험을 주지 않으면서 효과를 검증하고 문제를 식별하게 해줍니다. 권장 전략은 Virtual Threads용 엔드포인트를 전통적 엔드포인트로부터 분리하는 것입니다.

DualExecutorConfig.javajava
// Configuration for progressive migration
@Configuration
public class DualExecutorConfig {

    // Traditional executor for legacy endpoints
    @Bean("platformExecutor")
    public ExecutorService platformExecutor() {
        return Executors.newFixedThreadPool(200);
    }

    // Virtual Threads executor for new endpoints
    @Bean("virtualExecutor")
    public ExecutorService virtualExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

@RestController
@RequestMapping("/api/v2")
public class MigratedController {

    @Qualifier("virtualExecutor")
    private final ExecutorService executor;

    // Endpoint migrated to Virtual Threads
    @GetMapping("/users/{id}")
    public CompletableFuture<User> getUser(@PathVariable Long id) {
        return CompletableFuture.supplyAsync(() -> {
            // Business code unchanged
            return userService.findById(id);
        }, executor);
    }
}
yaml
# application.yml
# Feature flag for migration
features:
  virtual-threads:
    enabled: true
    endpoints:
      - /api/v2/**
      - /api/reports/**

# Disable for quick rollback
# features.virtual-threads.enabled: false
주의

모든 의존성을 검증하기 전에 Virtual Threads를 전역으로 활성화해서는 안 됩니다. pinning은 성능을 크게 떨어뜨릴 수 있습니다.

질문 12: 운영 배포 전에 어떤 테스트를 수행해야 하는가

Virtual Threads 검증에는 부하 테스트, pinning 테스트, 호환성 테스트가 필요합니다. 이러한 테스트는 현실적인 운영 조건을 모사해야 합니다.

VirtualThreadLoadTest.javajava
// Load test with JUnit and Virtual Threads
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class VirtualThreadLoadTest {

    @LocalServerPort
    private int port;

    @Test
    void shouldHandleHighConcurrency() throws Exception {
        int concurrentRequests = 5000;
        CountDownLatch latch = new CountDownLatch(concurrentRequests);
        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger errorCount = new AtomicInteger(0);

        HttpClient client = HttpClient.newHttpClient();

        // Launch 5000 simultaneous requests
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < concurrentRequests; i++) {
                executor.submit(() -> {
                    try {
                        HttpRequest request = HttpRequest.newBuilder()
                            .uri(URI.create("http://localhost:" + port + "/api/test"))
                            .build();

                        HttpResponse<String> response = client.send(
                            request, BodyHandlers.ofString());

                        if (response.statusCode() == 200) {
                            successCount.incrementAndGet();
                        } else {
                            errorCount.incrementAndGet();
                        }
                    } catch (Exception e) {
                        errorCount.incrementAndGet();
                    } finally {
                        latch.countDown();
                    }
                });
            }
        }

        latch.await(60, TimeUnit.SECONDS);

        // Performance assertions
        assertThat(successCount.get()).isGreaterThan(4900); // >98% success
        assertThat(errorCount.get()).isLessThan(100);
    }
}

테스트는 경합 시나리오(커넥션 풀 포화), 타임아웃, 수 분간 지속되는 부하 등을 포괄해야 합니다.

심화 질문

질문 13: Virtual Threads는 Structured Concurrency와 어떻게 상호작용하는가

Structured Concurrency(JEP 453)는 Virtual Threads를 보완하여 동시 작업이 동일한 라이프사이클을 공유하도록 보장합니다. 이 접근 방식은 오류 처리와 취소를 단순화합니다.

StructuredConcurrencyExample.javajava
// Combining Virtual Threads + Structured Concurrency
public class StructuredConcurrencyExample {

    public UserDashboard fetchDashboard(Long userId) throws Exception {
        // StructuredTaskScope ensures all tasks complete together
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

            // Three parallel tasks on Virtual Threads
            Subtask<UserProfile> profileTask = scope.fork(() ->
                userService.getProfile(userId));

            Subtask<List<Notification>> notifTask = scope.fork(() ->
                notificationService.getRecent(userId));

            Subtask<AccountBalance> balanceTask = scope.fork(() ->
                accountService.getBalance(userId));

            // Wait for all tasks or fail on first error
            scope.join();
            scope.throwIfFailed();

            // All tasks succeeded - safe aggregation
            return new UserDashboard(
                profileTask.get(),
                notifTask.get(),
                balanceTask.get()
            );
        }
        // If one task fails, others are automatically cancelled
    }
}

Structured Concurrency는 스레드 누수를 방지하고 콜 스택이 코드의 논리 구조를 반영하기 때문에 디버깅을 단순하게 만듭니다.

질문 14: Virtual Threads와 리액티브 프로그래밍의 차이는 무엇인가

두 접근 방식 모두 동일한 문제(I/O 효율)를 다루지만 프로그래밍 모델이 다릅니다. Virtual Threads는 전통적인 명령형 코드를 허용하는 반면 리액티브 프로그래밍은 스트림 형태로 다시 작성해야 합니다.

ComparisonService.javajava
// Same logic: imperative vs reactive
@Service
public class ComparisonService {

    // VIRTUAL THREADS APPROACH: classic imperative code
    // Easy to read, debug, and maintain
    public UserData getUserDataImperative(Long id) {
        User user = userRepository.findById(id).orElseThrow();
        List<Order> orders = orderRepository.findByUserId(id);
        PaymentInfo payment = paymentService.getInfo(id);

        return new UserData(user, orders, payment);
    }

    // REACTIVE APPROACH: streams and operators
    // More complex but native backpressure
    public Mono<UserData> getUserDataReactive(Long id) {
        return userRepository.findById(id)
            .zipWith(orderRepository.findByUserId(id).collectList())
            .zipWith(paymentService.getInfo(id))
            .map(tuple -> new UserData(
                tuple.getT1().getT1(),
                tuple.getT1().getT2(),
                tuple.getT2()
            ));
    }
}

| 기준 | Virtual Threads | 리액티브 | |------|-----------------|---------| | 학습 곡선 | 낮음 | 높음 | | 디버깅 | 전통적인 스택 트레이스 | 복잡 | | 백프레셔 | 수동 | 네이티브 | | 생태계 | 성장 중 | 성숙 | | 레거시 마이그레이션 | 단순 | 재작성 |

신규 애플리케이션과 마이그레이션에는 Virtual Threads가 권장됩니다. 정교한 백프레셔가 필요한 경우에는 리액티브가 여전히 유효합니다.

질문 15: Virtual Threads에서 ThreadLocal은 어떻게 다루는가

ThreadLocal은 Virtual Threads와 함께 사용할 수 있지만 인스턴스마다 메모리를 소비합니다. Scoped Values(JEP 446)는 컨텍스트 공유를 위한 더 효율적인 대안입니다.

ThreadLocalVsScopedValue.javajava
// Comparing context approaches
public class ThreadLocalVsScopedValue {

    // CLASSIC APPROACH: ThreadLocal
    // Works but expensive with millions of VT
    private static final ThreadLocal<RequestContext> requestContext =
        new ThreadLocal<>();

    public void processWithThreadLocal(Request request) {
        requestContext.set(new RequestContext(request.getTraceId()));
        try {
            // Context accessible everywhere in the thread
            processRequest();
        } finally {
            requestContext.remove(); // Important to prevent leaks
        }
    }

    // MODERN APPROACH: ScopedValue (Java 21+)
    // More efficient, immutable, explicit scope
    private static final ScopedValue<RequestContext> CONTEXT =
        ScopedValue.newInstance();

    public void processWithScopedValue(Request request) {
        ScopedValue.where(CONTEXT, new RequestContext(request.getTraceId()))
            .run(() -> {
                // Context accessible within this scope
                processRequest();
                // No cleanup needed - automatic at scope end
            });
    }

    private void processRequest() {
        // Context access
        String traceId = CONTEXT.isBound()
            ? CONTEXT.get().getTraceId()
            : "unknown";
        log.info("Processing with trace: {}", traceId);
    }
}

ScopedValues는 신규 개발에 권장됩니다. ThreadLocal을 사용하는 레거시 코드는 점진적인 마이그레이션이 바람직합니다.

Spring Boot 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

결론

Virtual Threads는 단순하면서 고성능인 코드를 가능하게 하여 자바 백엔드 개발을 변화시킵니다. 핵심 요점:

기초:

  • spring.threads.virtual.enabled=true로 활성화
  • I/O 바운드 워크로드(REST, DB, 파일)에 이상적
  • CPU 집약적인 계산에는 부적합

성능:

  • 커넥션 풀의 크기를 적절히 조정(새로운 병목)
  • -Djdk.tracePinnedThreads로 pinning 모니터링
  • synchronizedReentrantLock으로 마이그레이션

마이그레이션:

  • 엔드포인트 그룹 단위로 점진적으로 테스트
  • 의존성의 호환성 검증
  • 병렬 작업에는 Structured Concurrency 활용

Virtual Threads에 대한 숙련도는 현대 애플리케이션의 성능 과제를 이해하는 지원자를 돋보이게 합니다. 이러한 개념은 이제 Spring Boot 기술 면접에서 빠질 수 없는 요소입니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#spring boot
#virtual threads
#java 21
#project loom
#performance

공유

관련 기사