Contents
see ListJava의 현재: 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=trueVirtual 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.0Structured 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를 선택하세요.