본문으로 건너뛰기
목록으로

JDK 25(LTS) — JDK 21에서 무엇이 달라졌나: 정식이 된 언어 기능과 런타임 이득

Johny Cho
Software Engineer @ Kurly

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
런타임 변화는 코드를 바꾸지 않아도 얻는 이득입니다 — 25로 올린 뒤 플래그만 적용하면 힙·시작 속도에서 이점을 봅니다.

아직 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를 공식 지원 런타임으로 삼아 빌드·테스트·문서에서 완전히 검증합니다).
JDK 25의 가치는 미리보기 문법이 아니라 정식 기능에 있습니다 — 정식 기능으로 코드를 줄이고, 버전만 올려도 힙·시작 속도 같은 런타임 이득을 얻는 LTS입니다.