JDK 25 핵심 기능 — 다섯 번째 LTS, Compact Source Files와 Compact Headers

다섯 번째 LTS

2025년 9월 16일 출시 예정인 JDK 25는 다섯 번째 LTS(Long-Term Support) 릴리스입니다. JDK 8 → 11 → 17 → 21 → 25로 이어지는 LTS 계보에서, JDK 25는 그동안 프리뷰를 거쳐 온 핵심 기능들이 대거 정식 확정되는 버전입니다.

특히 Compact Source Files(JEP 512)는 Java의 진입 장벽을 근본적으로 낮추고, Compact Object Headers(JEP 519)는 JVM의 메모리 효율을 획기적으로 개선합니다. LTS 버전답게 실무 전환을 고려할 만한 변화가 집중되어 있습니다.

Compact Source Files and Instance Main Methods (JEP 512, 정식)

Java를 처음 배울 때 가장 큰 허들은 “Hello World”를 출력하기 위해 public class, public static void main(String[] args) 같은 보일러플레이트를 외워야 한다는 것이었습니다. JDK 25에서 이 문제가 해결됩니다.

// HelloJdk25.java — JDK 25 Compact Source File
// 클래스 선언 없이, static 없이 바로 실행 가능
void main() {
    // IO.println()은 System.out.println()의 간결한 대안
    IO.println("Hello, JDK 25!");
    // 출력: Hello, JDK 25!

    // 변수 선언과 사용도 클래스 없이 가능
    var languages = java.util.List.of("Java", "Kotlin", "Scala");
    IO.println("JVM 언어: " + languages);
    // 출력: JVM 언어: [Java, Kotlin, Scala]

    // 인스턴스 메서드이므로 this 참조 가능
    greet("개발자");
}

void greet(String name) {
    IO.println(name + "님, 환영합니다!");
    // 출력: 개발자님, 환영합니다!
}

기존 방식과 비교하면 차이가 극명합니다.

// 기존 방식 (JDK 24 이전)
public class HelloTraditional {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}

// JDK 25 방식
// HelloCompact.java
void main() {
    IO.println("Hello, Java!");
}

핵심 변화를 정리하면 다음과 같습니다.

항목기존JDK 25
클래스 선언public class Foo { } 필수생략 가능
main 메서드public static void main(String[] args)void main()
출력System.out.println()IO.println()
메서드 성격static인스턴스 메서드

이 기능은 교육용만이 아닙니다. 빠른 프로토타이핑, 스크립트성 유틸리티 작성에도 유용합니다. 물론 기존 public static void main(String[] args) 방식도 완벽하게 호환됩니다.

Scoped Values (JEP 506, 정식)

ThreadLocal은 Java의 스레드별 데이터 저장소로 오랫동안 사용되어 왔지만, 몇 가지 근본적인 문제가 있습니다. 값을 설정한 후 remove()를 호출하지 않으면 메모리 누수가 발생하고, 자식 스레드에 값을 상속하는 과정에서 예상치 못한 동작이 생기기도 합니다. Virtual Thread 환경에서는 수백만 개의 스레드가 생성되므로, ThreadLocal의 메모리 문제가 더욱 심각해집니다.

Scoped Values는 이 문제를 불변성범위 한정으로 해결합니다.

import java.util.concurrent.StructuredTaskScope;

public class ScopedValueDemo {
    // ScopedValue: 불변, 범위 한정, 자동 해제
    private static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
    private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    public static void main(String[] args) throws Exception {
        // ScopedValue.where()로 값 바인딩 — run() 블록이 끝나면 자동 해제
        ScopedValue.where(CURRENT_USER, "홍길동")
            .where(REQUEST_ID, "REQ-2025-001")
            .run(() -> {
                System.out.println("현재 사용자: " + CURRENT_USER.get());
                System.out.println("요청 ID: " + REQUEST_ID.get());
                // 출력:
                // 현재 사용자: 홍길동
                // 요청 ID: REQ-2025-001

                // 하위 메서드에서도 접근 가능
                processRequest();
            });

        // 범위 밖에서는 값이 존재하지 않음
        System.out.println("범위 밖 — 바인딩 존재? " + CURRENT_USER.isBound());
        // 출력: 범위 밖 — 바인딩 존재? false
    }

    static void processRequest() {
        // 메서드 호출 체인을 따라 자동 전파 — 파라미터로 전달할 필요 없음
        String user = CURRENT_USER.get();
        String reqId = REQUEST_ID.get();
        System.out.println("[" + reqId + "] " + user + "의 요청 처리 중...");
        // 출력: [REQ-2025-001] 홍길동의 요청 처리 중...
    }
}

ThreadLocalScopedValue의 차이를 정리하면 다음과 같습니다.

항목ThreadLocalScopedValue
가변성가변 (set/get)불변 (바인딩 후 변경 불가)
생명주기명시적 remove() 필요블록 종료 시 자동 해제
상속InheritableThreadLocalStructuredTaskScope와 자동 연동
Virtual Thread메모리 부담 큼경량

Compact Object Headers (JEP 519, 정식)

모든 Java 객체에는 **객체 헤더(Object Header)**가 붙습니다. GC 정보, 해시코드, 잠금 상태 등을 저장하는 메타데이터 영역인데, 기존에는 12바이트(64비트 JVM, Compressed Oops)를 차지했습니다.

JDK 25에서 이 헤더가 8바이트로 압축됩니다. 4바이트가 줄어드는 것이 사소해 보일 수 있지만, JVM 위에서 실행되는 모든 객체에 적용되므로 효과는 누적됩니다.

import java.util.ArrayList;
import java.util.List;

public class CompactHeadersDemo {
    // 작은 객체일수록 헤더 비율이 높아 효과가 큼
    record Point(int x, int y) {} // 객체 크기: 헤더(8B) + int(4B) + int(4B) = 16B

    public static void main(String[] args) {
        // 100만 개의 Point 객체 생성
        Runtime runtime = Runtime.getRuntime();
        runtime.gc(); // GC 실행하여 기준점 설정

        long beforeMemory = runtime.totalMemory() - runtime.freeMemory();

        List<Point> points = new ArrayList<>();
        for (int i = 0; i < 1_000_000; i++) {
            points.add(new Point(i, i * 2));
        }

        long afterMemory = runtime.totalMemory() - runtime.freeMemory();
        long usedMB = (afterMemory - beforeMemory) / (1024 * 1024);

        System.out.println("=== Compact Object Headers 효과 ===");
        System.out.println("생성된 객체 수: " + points.size());
        System.out.println("사용된 메모리: ~" + usedMB + " MB");
        System.out.println();

        // JDK 24 이전: 헤더 12B + 필드 8B = 20B → 패딩으로 24B (정렬)
        // JDK 25:     헤더  8B + 필드 8B = 16B → 패딩 없음
        long oldEstimate = 1_000_000L * 24 / (1024 * 1024);
        long newEstimate = 1_000_000L * 16 / (1024 * 1024);
        System.out.println("기존 예상 (헤더 12B): ~" + oldEstimate + " MB");
        System.out.println("JDK 25 예상 (헤더 8B): ~" + newEstimate + " MB");
        System.out.println("절감율: ~" + ((oldEstimate - newEstimate) * 100 / oldEstimate) + "%");
        // 출력 예시:
        // === Compact Object Headers 효과 ===
        // 생성된 객체 수: 1000000
        // 사용된 메모리: ~15 MB
        //
        // 기존 예상 (헤더 12B): ~22 MB
        // JDK 25 예상 (헤더 8B): ~15 MB
        // 절감율: ~31%
    }
}

SPECjbb 벤치마크 기준으로 측정된 공식 수치는 다음과 같습니다.

지표개선 폭
힙 사용량22% 감소
CPU 사용량8% 감소
GC 부하15% 감소

별도 코드 변경 없이 JDK 25로 업그레이드하는 것만으로 이 성능 향상을 얻을 수 있습니다.

Flexible Constructor Bodies (JEP 513, 정식)

기존 Java에서는 생성자 첫 줄에 반드시 super() 또는 this()를 호출해야 했습니다. 부모 생성자 호출 전에 인자를 검증하려면 별도의 static 팩토리 메서드를 만들어야 했습니다.

JDK 25에서는 super() 호출 전에 검증 코드를 작성할 수 있습니다.

import java.util.Objects;

public class FlexibleConstructorDemo {

    // 부모 클래스
    static class Animal {
        final String name;
        final int age;

        Animal(String name, int age) {
            this.name = name;
            this.age = age;
            System.out.println("Animal 생성: " + name + " (" + age + "세)");
        }
    }

    // JDK 25: super() 호출 전에 검증 코드 작성 가능
    static class Dog extends Animal {
        final String breed;

        Dog(String name, int age, String breed) {
            // super() 호출 전에 인자 검증 — JDK 24 이전에는 컴파일 에러!
            Objects.requireNonNull(name, "이름은 필수입니다");
            Objects.requireNonNull(breed, "품종은 필수입니다");
            if (age < 0 || age > 30) {
                throw new IllegalArgumentException("나이는 0~30 사이여야 합니다: " + age);
            }

            // 검증 통과 후 부모 생성자 호출
            super(name, age);
            this.breed = breed;
            System.out.println("Dog 생성: 품종=" + breed);
        }
    }

    public static void main(String[] args) {
        // 정상 생성
        Dog dog = new Dog("바둑이", 5, "진돗개");
        System.out.println(dog.name + " / " + dog.breed);
        // 출력:
        // Animal 생성: 바둑이 (5세)
        // Dog 생성: 품종=진돗개
        // 바둑이 / 진돗개

        // 유효하지 않은 인자 — super() 호출 전에 검증
        try {
            new Dog("멍멍이", -1, "시바견");
        } catch (IllegalArgumentException e) {
            System.out.println("검증 실패: " + e.getMessage());
            // 출력: 검증 실패: 나이는 0~30 사이여야 합니다: -1
        }
    }
}

이전에는 이런 검증을 위해 static 팩토리 메서드를 만들거나, 검증 로직을 super() 인자 안에 삼항 연산자로 넣는 등의 우회 방법을 사용했습니다. JDK 25에서는 자연스럽게 “검증 먼저, 초기화 나중”이 가능해집니다.

그 외 주목할 JEP

Module Import Declarations (JEP 511, 정식): JDK 23에서 프리뷰로 시작된 import module 구문이 정식 확정되었습니다. import module java.base; 한 줄로 java.util, java.io, java.time 등 전체 패키지를 임포트할 수 있습니다.

AOT 간소화 + Method Profiling (JEP 514/515): AOT(Ahead-of-Time) 클래스 로딩 설정이 간소화되고, 메서드 프로파일링 데이터를 활용하여 시작 시간이 약 19% 개선됩니다.

Generational Shenandoah (JEP 518, 정식): ZGC에 이어 Shenandoah GC도 세대별 모드가 정식화되었습니다. 저지연 GC 선택지가 더 풍부해졌습니다.

Key Derivation Function API (JEP 510, 정식): HKDF 같은 키 파생 함수를 위한 표준 API가 추가되었습니다. 양자내성 암호화(JDK 24)와 함께 Java의 보안 API 현대화가 이어지고 있습니다.

정리

기능상태핵심 포인트
Compact Source Files정식void main(), IO.println(), 클래스 선언 불필요
Scoped Values정식ThreadLocal 대안, 불변, 자동 해제
Compact Object Headers정식헤더 12B → 8B, 힙 22%↓, CPU 8%↓
Flexible Constructor Bodies정식super() 전에 인자 검증 가능
Module Import정식import module java.base;
AOT 개선정식시작 시간 19% 개선
Generational Shenandoah정식Shenandoah GC 세대별 모드

JDK 25는 LTS 버전으로서 향후 수년간 프로덕션 환경의 기준이 될 릴리스입니다. Compact Object Headers 하나만으로도 업그레이드 가치가 충분하며, Scoped Values와 Flexible Constructor Bodies는 코드 품질을 즉시 개선해줍니다. 현재 JDK 21 LTS를 사용 중이라면, JDK 25 LTS로의 마이그레이션 계획을 지금부터 수립하는 것을 권장합니다.

이 글이 도움이 되었나요?