1. 서론
사이드 프로젝트를 새로 진행하며 Java / Spring 버전에 대한 스택을 정하려고 했는데요.
최근 2025년 9월에 출시한 Java 25에 대한 내용을 정리 해봤습니다.
25버전은 LTS 버전으로 출시되었으며, 스탠다드 라이브러리, API와 런타임에 대한 변경점이 있었습니다.
2. JEP 506 - Scoped Values / ThreadLocal 대체
기존에 ThreadLocal은 누수와 복잡한 라이프사이클 관리 문제가 있었다고 합니다. Java 25의 Scoped Values는 읽기 전용 컨텍스트 값을 블록 범위로 안전하게 전달하는 방식이라, 비동기·가상 스레드에서도 깔끔하게 동작한다고 하는데요. 아래의 예를 살펴보겠습니다.
import java.lang.scoped.ScopedValue;
public class ScopedExample {
static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
public static void main(String[] args) {
ScopedValue.where(USER_ID, "user-123")
.run(() -> {
log(); // 항상 user-123
new Thread(ScopedExample::log).start(); // 전달된 컨텍스트로 실행
});
}
static void log() {
System.out.println("userId = " + USER_ID.orElse("anonymous"));
}
}
- ThreadLocal처럼 수동으로 remove() 할 필요가 없고, 범위를 벗어나면 자동으로 사라진다.
- 가상 스레드/구조적 동시성과 조합하면 트레이싱 컨텍스트, correlationId 전달에 좋다.
3. JEP 505 - Structured Concurrency / 병렬 작업을 하나의 작업 단위로 전환
여러 스레드를 띄워서 처리할 때, 예외 처리, 취소, 타임아웃 관리가 헬이었는데, Structured Concurrency는 “이 블록에서 시작한 작업은 이 블록 안에서 반드시 끝난다”는 모델을 제공한다.
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope;
public class StructuredExample {
public static void main(String[] args) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> loadUser());
Future<String> orders = scope.fork(() -> loadOrders());
scope.join(); // 둘 다 끝날 때까지
scope.throwIfFailed(); // 하나라도 실패하면 예외
System.out.println(user.resultNow() + " / " + orders.resultNow());
}
}
static String loadUser() { return "user"; }
static String loadOrders() { return "orders"; }
}
- 예외가 나면 나머지 작업은 자동으로 취소되고, join()/throwIfFailed()에서 정리된다.
- MSA 호출 묶음(유저 정보 + 주문 + 포인트 등)을 한 “논리 트랜잭션”으로 관리하기 좋다.
4. JEP 507 - 원시 타입 패턴 매칭 강화
이전에는 패턴 매칭이 참조 타입 중심이었는데 Java 25에서는 switch나 instanceof에서 원시 자료형 타입까지 다룰 수 있다고 합니다.
static void printValue(Object obj) {
switch (obj) {
case Integer i -> System.out.println("정수: " + i);
case Double d -> System.out.println("실수: " + d);
case String s -> System.out.println("문자열: " + s);
default -> System.out.println("기타 타입");
}
}
- 복잡한 if (obj instanceof Integer) 체인을 switch 하나로 정리 가능.
- 도메인 이벤트 타입 분기, 다양한 값 타입 처리 시 가독성이 올라간다.
5. JEP 447/492 - super() 호출 전 검증 가능
기존에는 생성자에서 super() 호출 전에는 어떠한 코드가 있더래도 컴파일 에러가 발생했습니다. 하지만 Java 25에서는 부모 생성자 호출 전에 검증, 전처리 코드를 허용해서 도메인 규칙을 생성자 초기에 명확히 둘 수 있다고 합니다.
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
// super() 전에 검증 가능
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 필수");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("유효하지 않은 나이");
}
super(); // 이제 검증 뒤에 올 수 있음
this.name = name.trim();
this.age = age;
}
}
- 생성자에서 도메인 유효성 검증을 직관적인 순서로 작성 가능.
- 복잡한 상속 구조에서도 “검증 → 부모 초기화 → 필드 세팅” 흐름을 깔끔하게 유지한다.
6. JEP 519 - Compact Object Headers / 메모리 효율 개선
JVM 내부에서 객체 헤더를 더 컴팩트하게 만들어, 객체당 메모리 오버헤드를 줄인 것이 큰 변화 중 하나다.
- 일반적인 64비트 환경에서 객체 헤더를 96비트 수준에서 64비트로 줄이는 최적화가 적용되었다.
- 실제로는 GC, Compressed Oops, 헤더 레이아웃 최적화가 결합되어 컬렉션·도메인 객체가 대량으로 생성되는 서비스에서 힙 사용량 감소 + 캐시 효율 증가 효과가 난다.
예를 들어서 대량 트래픽이 들어오는 주문 서비스에서 Order, OrderItem 객체를 수백만 개 생성하는 경우, 같은 힙 크기에서 더 많은 객체를 처리할 수 있게 되고, GC 횟수가 줄어 레이턴시가 개선될 수 있습니다.
7. JEP 508/518/509 - Vector API / JFR 개선 성능, 프로파일링 업그레이드
- Vector API (인큐베이터 지속): SIMD 명령(하나의 명령어로 여러 데이터를 동시에 처리하는 병렬 컴퓨팅 기술)을 활용한 벡터 연산을 쉽게 사용해, 통계/로그 분석/배치 처리에서 순수 Java로도 C에 가까운 성능을 낼 수 있다.
- JFR(JDK Flight Recorder) CPU 시간 프로파일링, 메서드 타이밍/트레이싱: 운영 환경에서 CPU hotspot, 느린 메서드, GC 영향을 세밀하게 분석하기 쉬워졌다.
이 부분은 로그 집계 배치 작업에서 특정 함수가 CPU 사용률을 너무 사용하는 경우, JFR로 해당 메서드의 CPU 시간을 직접 보고, 벡터화나 알고리즘 개선을 적용하기가 더 좋아졌다고 합니다.
8. 마치며..?
정리하면 Java 25는 아래와 같은 크게 3가지 부분에 대해서 변경점이 있었습니다.
- 멀티스레딩 단순화(Scoped Values, Structured Concurrency)
- 언어 표현력 강화(패턴 매칭, 생성자 전 검증)
- 런타임 성능·메모리 효율 개선(Compact Headers, Vector API, GC 개선)
개인적으로 원시타입 매칭과 메모리 효율 개선에 대해서 주의 깊게 살펴보았습니다.
원래도 원시타입 매칭은 래퍼 클래스를 통해서 하는 경우도 있었지만, 가독성이 더 향상 될 것 같습니다.
또한, 다른 분들도 다들 눈여겨 보고 있는 메모리 효율 개선을 통한 최적화 부분은 개인적으로 실험해보면 좋을 것 같다고 느꼈습니다.
'Code > Java' 카테고리의 다른 글
| Stream API를 무조건 쓰는 게 정답일까? (함수형 프로그래밍 with Java) (0) | 2025.11.29 |
|---|