Java의 현재: LTS 버전 전략과 최신 흐름

Java는 2017년부터 6개월 릴리스 주기를 도입하여 빠르게 발전하고 있습니다. 2024년 9월 출시된 Java 21 (LTS)은 Virtual Threads, Record Patterns, Sequenced Collections 등 혁신적인 기능을 담았고, 2025년 9월 출시된 Java 25 (LTS)은 AOT 컴파일 최적화와 Structured Concurrency 완성으로 성능을 한 단계 끌어올렸습니다.

Virtual Threads: 동시성 프로그래밍의 혁명

Java 21에서 정식 출시된 Virtual Threads(JEP 444)는 수십만 개의 동시 작업을 처리할 수 있는 경량 스레드입니다. 기존 플랫폼 스레드는 OS 스레드와 1:1 매핑되어 메모리(약 1MB/스레드)와 컨텍스트 전환 비용이 컸지만, Virtual Thread는 JVM이 직접 관리하며 수KB 수준의 메모리만 사용합니다.

// 기존 방식: 플랫폼 스레드 (OS 스레드 기반)
ExecutorService executor = Executors.newFixedThreadPool(200);
for (int i = 0; i < 10000; i++) {
    executor.submit(() -> {
        // 각 스레드가 OS 리소스 점유
        Thread.sleep(1000);
        processRequest();
    });
}
// 200개 제한으로 나머지 9800개는 대기

// Virtual Thread 방식
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100000; i++) {
        executor.submit(() -> {
            // 10만개 Virtual Thread 동시 실행 가능
            Thread.sleep(1000);
            processRequest();
        });
    }
}
// 메모리 효율적, 블로킹 I/O 자동 양보

// Virtual Thread 직접 생성
Thread vt = Thread.ofVirtual()
    .name("worker-", 0)
    .start(() -> {
        System.out.println("Virtual Thread: " + Thread.currentThread().isVirtual());
    });

// Spring Boot 3.2+ 설정
// application.properties
// spring.threads.virtual.enabled=true

Virtual Thread 주의사항: Pinning 해결

Java 21-23에서는 synchronized 블록 안에서 블로킹 작업 시 Virtual Thread가 플랫폼 스레드에 고정(pinning)되는 문제가 있었습니다. Java 24의 JEP 491로 이 문제가 크게 개선되었습니다.

// 나쁜 예: synchronized + 블로킹 (Java 21-23에서 pinning 발생)
public synchronized void processWithBlock() {
    Thread.sleep(1000); // pinning 발생!
}

// 좋은 예: ReentrantLock 사용 (pinning 없음)
private final ReentrantLock lock = new ReentrantLock();

public void processWithLock() throws InterruptedException {
    lock.lock();
    try {
        Thread.sleep(1000); // Virtual Thread 정상 양보
    } finally {
        lock.unlock();
    }
}

// Java 24+: synchronized 개선으로 아래도 안전
public synchronized void processModern() throws InterruptedException {
    Thread.sleep(1000); // JEP 491 이후 pinning 감소
}

Record Patterns: 선언적 데이터 분해

Java 21에서 정식 출시된 Record Patterns는 instanceof 패턴 매칭과 결합하여 복잡한 데이터 구조를 선언적으로 분해할 수 있습니다.

// Record 정의
record Point(int x, int y) {}
record Circle(Point center, double radius) {}
record Rectangle(Point topLeft, Point bottomRight) {}

sealed interface Shape permits Circle, Rectangle {}

// 기존 방식 (Java 16 이전)
void processShape_old(Shape shape) {
    if (shape instanceof Circle) {
        Circle c = (Circle) shape;
        Point p = c.center();
        System.out.println("원: 중심=" + p.x() + "," + p.y() + " 반지름=" + c.radius());
    } else if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle) shape;
        // ...
    }
}

// Record Patterns + switch 표현식 (Java 21)
void processShape(Shape shape) {
    switch (shape) {
        case Circle(Point(int x, int y), double radius) ->
            System.out.printf("원: 중심=(%d,%d) 반지름=%.1f%n", x, y, radius);
        
        case Rectangle(Point(int x1, int y1), Point(int x2, int y2)) -> {
            int width = Math.abs(x2 - x1);
            int height = Math.abs(y2 - y1);
            System.out.printf("사각형: %dx%d (넓이=%d)%n", width, height, width * height);
        }
    }
}

// 사용 예
Shape circle = new Circle(new Point(3, 4), 5.0);
processShape(circle); // 원: 중심=(3,4) 반지름=5.0

Structured Concurrency: 안전한 동시 작업

Java 21-24에서 Preview를 거쳐 Java 25에서 완성된 Structured Concurrency는 여러 동시 작업의 생명주기를 명확하게 관리합니다.

import java.util.concurrent.StructuredTaskScope;

// 사용자 정보 + 주문 목록을 병렬로 가져오기
public record UserOrderInfo(User user, List orders) {}

public UserOrderInfo fetchUserOrderInfo(long userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        
        // 두 작업을 병렬 실행
        StructuredTaskScope.Subtask userTask = 
            scope.fork(() -> userService.findById(userId));
        
        StructuredTaskScope.Subtask> ordersTask = 
            scope.fork(() -> orderService.findByUserId(userId));
        
        // 모든 작업 완료 대기
        scope.join().throwIfFailed();
        
        // 하나라도 실패하면 나머지 자동 취소
        return new UserOrderInfo(userTask.get(), ordersTask.get());
    }
}

// ShutdownOnSuccess: 가장 먼저 성공한 결과 사용 (경쟁 패턴)
public String fetchFromFastest(String url) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) {
        scope.fork(() -> httpClient1.get(url));
        scope.fork(() -> httpClient2.get(url)); // 백업 서버
        
        scope.join();
        return scope.result(); // 가장 빠른 응답 반환
    }
}

Scoped Values: ThreadLocal 대체

Java 21에서 Preview, Java 25에서 정식 출시된 Scoped Values는 기존 ThreadLocal의 문제점(메모리 누수, Virtual Thread와 비호환)을 해결합니다.

// Scoped Values 정의
final static ScopedValue CURRENT_USER = ScopedValue.newInstance();
final static ScopedValue REQUEST_ID = ScopedValue.newInstance();

// 요청 처리 진입점
public void handleRequest(HttpRequest request) {
    User user = authenticate(request);
    String reqId = request.getHeader("X-Request-Id");
    
    ScopedValue.where(CURRENT_USER, user)
               .where(REQUEST_ID, reqId)
               .run(() -> processRequest(request));
    // 스코프 종료 시 자동 정리
}

// 중첩 메서드에서 접근
public void processRequest(HttpRequest request) {
    User user = CURRENT_USER.get(); // 전달받지 않아도 접근 가능
    String reqId = REQUEST_ID.get();
    
    logger.info("[{}] 요청 처리 중: userId={}", reqId, user.getId());
    // 비즈니스 로직 수행...
}

// Virtual Thread와 완벽 호환
try (var scope = new StructuredTaskScope<>()) {
    scope.fork(() -> {
        // 자식 Virtual Thread도 Scoped Value 상속
        User user = CURRENT_USER.get(); // 동작함
        return processSubTask(user);
    });
    scope.join();
}

Java 25 AOT 성능 최적화 (JEP 514, 515)

Java 25는 Ahead-of-Time(AOT) 컴파일 에르고노믹스와 Profile-Guided Optimization으로 애플리케이션 시작 시간을 15-25% 단축했습니다.

# JEP 515: AOT 훈련 실행으로 프로파일 생성
$ java -XX:AOTMode=record -XX:AOTConfiguration=myapp.aotconf -jar myapp.jar
# 애플리케이션 실행 후 종료

# AOT 캐시 생성
$ java -XX:AOTMode=create -XX:AOTConfiguration=myapp.aotconf \
       -XX:AOTCache=myapp.aot -jar myapp.jar

# AOT 캐시 적용하여 실행 (15-25% 빠른 시작)
$ java -XX:AOTCache=myapp.aot -jar myapp.jar

# 결과 비교
# 기존: 시작 시간 ~2.5초
# AOT 적용: 시작 시간 ~1.9초 (-24% 개선)

정리

Java 21과 Java 25 LTS는 Java의 현대화를 보여주는 중요한 이정표입니다. Virtual Threads로 고성능 I/O 처리가 가능해졌고, Record Patterns와 sealed 클래스로 타입 안전한 도메인 모델링이 편리해졌습니다. Structured Concurrency와 Scoped Values로 복잡한 동시성 코드를 구조적으로 관리할 수 있습니다. 새 프로젝트라면 Java 21 이상을, 최고 성능이 필요하다면 Java 25를 선택하세요.