JDK 25(LTS) — JDK 21에서 무엇이 달라졌나: 정식이 된 언어 기능과 런타임 이득
JDK 25는 2025-09-16 GA한 LTS(장기 지원) 버전입니다. JDK 21 다음 LTS라, 운영에서 21에 머무르던 팀이 다음으로 올릴 목적지입니다(이전 LTS 정리: JDK 17 · JDK 21). 이번엔 preview를 거쳐 정식(final)이 된 언어 기능이 여럿이라, 실무 코드에 바로 사용할 수 있는 것 위주로 정리합니다(아직 preview인 것은 뒤에서 따로 표시).
LTS에서 눈여겨볼 것은 미리보기 기능이 아니라 정식으로 확정된 기능입니다 — JDK 25에서 final이 된 것은--enable-preview 없이 바로 사용할 수 있습니다.
1. 모듈 임포트 — import module 한 줄 (JEP 511, 정식)
파일 맨 위 import 더미를 줄여 줍니다. 한 모듈이 export하는 모든 패키지를 한 줄로 가져옵니다.
// 기존 — 스트림 몇 줄 쓰려고 import가 코드만큼 길다
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// JDK 25 — java.base가 export하는 패키지를 한 번에
import module java.base;
import module java.base;는 on-demand 패키지 임포트 54개와 같은 효과입니다. 프로토타이핑·학습·여러 패키지에 걸친 라이브러리를 사용할 때 특히 편합니다.
2. 간결한 소스 파일 + 인스턴스 main (JEP 512, 정식)
"Hello World"를 작성하려면 클래스 선언·public static void main(String[] args)·System.out.println이 필요했습니다. JDK 25는 이 절차를 제거합니다.
// 클래스 선언도, static도, String[] args도 없이
void main() {
IO.println("Hello, JDK 25");
}
- 클래스 없이 파일에 바로 인스턴스
main()을 작성할 수 있고(컴파일러가 감춰진 클래스를 만듦),String[] args도 필요 없을 때 생략합니다. - 새
java.lang.IO의IO.println으로 출력합니다(System.out을 몰라도 됨). java Hello.java로 컴파일 없이 실행하던 흐름과 더해져, 입문·스크립트·예제의 진입 장벽을 낮춥니다.
3. 유연한 생성자 본문 (JEP 513, 정식)
그동안 생성자에서 super(...)/this(...)는 반드시 첫 문장이어야 했습니다. 그래서 인자 검증이나 필드 준비를 생성자 앞에서 못 하고, 편법(정적 헬퍼 메서드 등)을 사용했습니다. JDK 25는 super() 앞에 문장을 허용합니다.
class Employee extends Person {
Employee(int age) {
// 이제 super() 앞에서 검증·가공 가능
if (age < 18) {
throw new IllegalArgumentException("미성년 불가");
}
int normalized = Math.max(age, 18);
super(normalized); // 검증을 통과한 값으로 부모 생성자 호출
}
}
부모 객체를 만들기 전에 잘못된 인자를 잡아낼 수 있어, 불필요한 상위 초기화·부작용을 막습니다.
"검증은 super() 뒤에서"라는 오래된 제약이 사라져, 잘못된 입력을 부모 초기화 전에 차단할 수 있습니다.4. Scoped Values — ThreadLocal의 대안 (JEP 506, 정식)
요청 컨텍스트(현재 사용자·트랜잭션 ID 등)를 메서드 인자로 넘기지 않고 공유할 때 ThreadLocal을 사용했습니다. 그런데 ThreadLocal은 가변이고 정리(remove)를 깜빡하면 누수되며, 가상 스레드가 수백만 개면 부담이 됩니다. ScopedValue 는 불변 + 정해진 범위 안에서만 유효한 대안입니다.
final static ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
// where(...)로 값을 바인딩하고, run(...) 블록 안에서만 유효
ScopedValue.where(CURRENT_USER, user).run(() -> {
handle(); // 이 안 어디서든
});
void handle() {
User u = CURRENT_USER.get(); // 바인딩된 값 조회 (범위 밖이면 예외)
}
- 블록이 끝나면 자동으로 풀려 누수가 없고, 값이 불변이라 예측 가능합니다.
- 가상 스레드(가상 스레드는 JDK 21에서 정식에서 스레드 풀 맥락으로 언급)·구조적 동시성과 궁합이 좋습니다 — 하위 작업으로 값이 안전하게 전달됩니다.
5. 런타임 — 메모리·GC·시작 속도
언어 기능 외에 JVM 쪽도 정식화된 게 많습니다. 코드를 바꾸지 않고 실행 옵션(플래그)만으로 얻는 이득이라 운영에서 특히 유용합니다.
Compact Object Headers (JEP 519) — 힙 절감
64비트 JVM은 객체마다 헤더(힙 주소·상태 비트 등)를 붙이는데, 그동안 128비트였습니다. JEP 519는 이를 64비트로 절반 줄입니다. 객체가 많을수록 효과가 커서, 대표 벤치마크에서 힙 22% 감소·CPU 시간 8% 감소가 보고됐습니다. JDK 24에서 실험 기능이었다가 25에서 정식이 됐고, 플래그로 활성화합니다.
java -XX:+UseCompactObjectHeaders -jar app.jar
작은 객체(수억 개의 Long·짧은 문자열·트리 노드 등)를 많이 처리하는 서비스라면 GC 빈도와 메모리 여유에서 바로 차이가 납니다.
Generational Shenandoah (JEP 521) — 짧게 사는 객체를 자주 회수하는 GC
Shenandoah는 정지 시간이 짧은 GC인데, JDK 25에서 세대(generational) 모드가 정식이 됐습니다. "대부분의 객체는 짧게 쓰이고 버려진다"는 약한 세대 가설을 활용해, 젊은 객체만 자주 청소하고 오래 산 객체는 덜 건드립니다. 그 결과 같은 지연 목표에서 처리량·메모리 효율이 오릅니다.
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -jar app.jar
AOT 캐시 (JEP 514·515) — 시작 속도
JVM은 처음엔 인터프리터로 실행되다가 JIT 컴파일러가 최적화를 마쳐야 빨라집니다. 그래서 기동 직후가 가장 느립니다. Project Leyden 흐름의 AOT 캐시는, 대표 워크로드로 "훈련 실행"을 한 번 해 클래스 로딩 결과와 메서드 프로파일을 캐시에 저장해 두고, 다음 기동부터 그걸 재사용해 워밍업을 앞당깁니다.
- JEP 514는 이 캐시 생성을 한 단계로 간소화하고(예전엔 record → create 두 단계였습니다),
- JEP 515는 캐시에 메서드 프로파일까지 저장해, JIT가 시작하자마자 최적화된 네이티브 코드를 만들게 합니다.
# 1) 훈련 실행 한 번으로 캐시 생성 (JEP 514)
java -XX:AOTCacheOutput=app.aot -jar app.jar
# 2) 캐시를 사용해 기동 — 클래스 로딩·JIT 프로파일이 준비돼 시작이 빠름 (JEP 515)
java -XX:AOTCache=app.aot -jar app.jar
파드가 자주 기동하는 오토스케일 환경이나 서버리스처럼 기동이 잦을수록 시작 지연 단축이 그대로 비용·응답성 이득입니다.
JFR 개선 (JEP 518·509) — 더 정확한 프로파일링
JFR(Java Flight Recorder) 는 JVM에 내장된 저부하 기록 도구입니다. 실행 중 발생하는 일(메서드 실행 표본·객체 할당·GC·락 대기·I/O 등)을 항공기 블랙박스처럼 아주 낮은 오버헤드로 계속 기록해 두고, 나중에 파일(.jfr)로 꺼내 JDK Mission Control 같은 도구로 분석합니다. 운영 중인 서버에 활성화해 둬도 성능 부담이 거의 없어, "느려졌을 때 무엇이 원인이었나"를 사후에 들여다볼 수 있습니다. JDK 25에서는 이 JFR의 정확도가 올랐습니다.
- 협조적 샘플링(JEP 518) — 스택 표본을 세이프포인트에서만 떠 표본 편향(safepoint bias) 을 줄입니다. "어디서 시간을 사용하나"가 더 정확해집니다.
- CPU-Time 프로파일링(JEP 509, Linux·실험적) — 경과 시간(wall-clock)이 아니라 실제 CPU 사용 시간 기준으로 표본을 떠, CPU를 실제로 많이 사용하는 지점을 더 정확히 가려냅니다.
# 60초간 기록 → rec.jfr (JDK Mission Control 등으로 분석)
java -XX:StartFlightRecording=filename=rec.jfr,duration=60s -jar app.jar
아직 preview — 실험만, 운영 금지
아래는 JDK 25에서 preview/incubator라 --enable-preview가 필요하고 버전 간 문법이 바뀔 수 있습니다. 운영 코드엔 넣지 말고 검토만 합니다.
- 구조적 동시성(JEP 505, 5차 preview) — 여러 하위 작업을 하나의 단위로 묶어 실패·취소를 함께 다룸. 가상 스레드와 짝. 아직 API가 다듬어지는 중.
- Stable Values(JEP 502, preview) — 한 번만 초기화되는 불변 값(지연 상수).
final보다 유연한 지연 초기화. - 패턴에서 원시 타입(JEP 507, 3차 preview) —
switch·instanceof패턴에서int같은 원시 타입 매칭.
정리 — 21에서 25로
- 바로 사용하는 정식 기능:
import module(JEP 511), 간결한 소스 파일 + 인스턴스main(512), 유연한 생성자 본문(513), Scoped Values(506). - 코드 없이 얻는 런타임 이득: Compact Object Headers·Generational Shenandoah·AOT 시작 속도.
- 아직 preview: 구조적 동시성·Stable Values·패턴의 원시 타입 — 실험만.
- LTS라 지원이 길고, 정식 기능은 대부분 소스 호환이라 21에서 넘어갈 명분이 큽니다(프레임워크 쪽에서도 Spring Boot 4.0이 Java 25를 공식 지원 런타임으로 삼아 빌드·테스트·문서에서 완전히 검증합니다).
