본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
37장 : 선택 심화 — Java 25 프리뷰 기능

프리뷰 기능 종료 전략

`class` 존재를 안정성으로 오판하는 반례를 고치고 JEP·JDK별 기능 목록, 안정 기준선, 어댑터, 삭제·승격 기준으로 프리뷰 실험을 관리합니다.

프리뷰는 채택을 약속하지 않습니다. 다음 릴리스에서 같은 이름으로 다시 프리뷰되더라도 시그니처와 의미가 달라질 수 있습니다. Java 25의 구조화된 동시성은 다섯 번째 프리뷰에서 open()Joiner 중심 API로 바뀌었고, Stable Values와 기본형 패턴도 후속 릴리스에서 계속 변화합니다. 소스를 유지하는 것보다 실험 질문과 관찰 근거를 유지해야 합니다.

클래스 존재와 안정성 오판

아래 확인 프로그램은 리플렉션으로 java.lang.StableValue를 찾고 존재하면 안정 기능이라고 출력합니다. JDK 25에는 해당 클래스가 실제 존재하지만 프리뷰 API이므로 결과 wrong-stable=true는 잘못된 판단입니다.

lab/PreviewPresenceIsNotStabilityBug.java
public final class PreviewPresenceIsNotStabilityBug {
    public static void main(String[] args) {
        boolean present;
        try {
            Class.forName("java.lang.StableValue");
            present = true;
        } catch (ClassNotFoundException exception) {
            present = false;
        }
        System.out.println("class-present=" + present);
        System.out.println("wrong-stable=" + present);
    }
}

원칙은 JEP 상태와 해당 릴리스의 명세를 판단 기준으로 삼는 것입니다. 리플렉션은 런타임에 기능이 존재하는지만 알려 주며 프리뷰·인큐베이터·실험·정식 기능의 구분을 제공하지 않습니다.

기능 목록의 릴리스·프리뷰 차수

문서 제목만 저장하지 말고 JEP 번호, JDK 릴리스, 상태, 프리뷰 반복, 컴파일·실행 플래그, 소유자, 재평가 날짜를 구조화합니다.

src/PreviewFeatureRegistry.java
import java.time.LocalDate;
import java.util.List;

public final class PreviewFeatureRegistry {
    private enum Status {
        PREVIEW,
        INCUBATOR,
        EXPERIMENTAL,
        STABLE
    }

    private record Feature(
            String name, int jep, int jdk, Status status, int iteration, LocalDate reviewDate) {}

    public static void main(String[] args) {
        List<Feature> features =
                List.of(
                        new Feature(
                                "Stable Values",
                                502,
                                25,
                                Status.PREVIEW,
                                1,
                                LocalDate.of(2026, 3, 1)),
                        new Feature(
                                "Structured Concurrency",
                                505,
                                25,
                                Status.PREVIEW,
                                5,
                                LocalDate.of(2026, 3, 1)),
                        new Feature(
                                "Primitive Patterns",
                                507,
                                25,
                                Status.PREVIEW,
                                3,
                                LocalDate.of(2026, 3, 1)),
                        new Feature(
                                "Scoped Values",
                                506,
                                25,
                                Status.STABLE,
                                0,
                                LocalDate.of(2027, 1, 1)));
        features.forEach(
                feature ->
                        System.out.println(
                                feature.name()
                                        + " JEP="
                                        + feature.jep()
                                        + " status="
                                        + feature.status()));
    }
}

목록은 빌드 플래그를 자동 결정하는 도구가 아니라 관리 기록입니다. OpenJDK JEP와 실제 설치된 JDK 컴파일 확인 프로그램을 함께 확인합니다.

안정 기준선·프리뷰 어댑터 비교

Stable Values를 평가할 때 기존 해법이 무엇인지 먼저 고정합니다. synchronized 이중 검사, 홀더 관용구, 즉시 초기화한 final 필드처럼 정식 기능으로 만든 기준선이 요구를 충족한다면 프리뷰 도입의 이익이 작을 수 있습니다.

src/SynchronizedLazyCellBaseline.java
import java.util.Objects;
import java.util.function.Supplier;

public final class SynchronizedLazyCellBaseline {
    private static final class LazyCell<T> {
        private volatile T value;

        T get(Supplier<? extends T> initializer) {
            T observed = value;
            if (observed != null) {
                return observed;
            }
            synchronized (this) {
                if (value == null) {
                    value = Objects.requireNonNull(initializer.get());
                }
                return value;
            }
        }
    }

    public static void main(String[] args) {
        LazyCell<String> cell = new LazyCell<>();
        System.out.println(cell.get(() -> "ready"));
        System.out.println(cell.get(() -> "must-not-replace"));
    }
}

프리뷰 구현은 동일한 작은 애플리케이션 소유 인터페이스 뒤에 둡니다. public 도메인 API가 JDK 프리뷰 타입을 직접 반환하면 철회 시 소비자 전체가 바뀝니다.

preview/StableValueLazyCellExperiment.java
import java.util.function.Supplier;

public final class StableValueLazyCellExperiment {
    private interface LazyCell<T> {
        T get(Supplier<? extends T> initializer);
    }

    private static final class PreviewCell<T> implements LazyCell<T> {
        private final StableValue<T> value = StableValue.of();

        @Override
        public T get(Supplier<? extends T> initializer) {
            return value.orElseSet(initializer);
        }
    }

    public static void main(String[] args) {
        LazyCell<String> cell = new PreviewCell<>();
        System.out.println(cell.get(() -> "ready"));
        System.out.println(cell.get(() -> "must-not-replace"));
    }
}

두 구현을 같은 동시성 시나리오와 초기화 실패 정책으로 비교합니다. API를 사용하는 코드가 짧다는 이유만으로 선택하지 말고 의미, 성능, 진단 측면의 이점을 측정합니다.

JDK 변경과 컴파일 재확인

새 JDK 도구 체인이 준비되면 이전 소스를 그대로 컴파일해 실패를 근거로 남긴 뒤 해당 릴리스의 JEP에 맞춰 실험 코드를 수정합니다. 하나의 소스를 조건부 리플렉션으로 여러 JDK에 억지로 맞추려 하지 않습니다.

src/PreviewReevaluationMatrix.java
import java.util.List;

public final class PreviewReevaluationMatrix {
    private record Run(
            int jdk,
            String feature,
            String sourceRevision,
            String compileCommand,
            String observed) {}

    public static void main(String[] args) {
        List<Run> runs =
                List.of(
                        new Run(
                                25,
                                "structured-concurrency",
                                "jdk25-open-api",
                                "javac --enable-preview --release 25",
                                "compiled"),
                        new Run(
                                25,
                                "stable-values",
                                "jep502-v1",
                                "javac --enable-preview --release 25",
                                "compiled"),
                        new Run(
                                25,
                                "primitive-patterns",
                                "jep507-v3",
                                "javac --enable-preview --release 25",
                                "compiled"));
        runs.forEach(System.out::println);
    }
}

승격·계속·삭제 결정

  • 정식 기능으로 승격되면 프리뷰 플래그를 제거하고 안정 소스로 다시 작성·컴파일합니다.
  • 다음 프리뷰에서 API가 바뀌면 이전 분기를 보존하고 마이그레이션 내역과 동작 차이를 기록합니다.
  • 요구 이익이 없거나 기능이 철회되면 어댑터 구현과 CI 작업을 삭제합니다.
  • 운영 코드가 실험에 의존하지 않아 삭제가 작은 변경인지 확인합니다.
  • 문서에는 “현재 JDK 25 프리뷰”라고 버전을 명시하고 영구 권장 API처럼 서술하지 않습니다.

실험 종료는 실패가 아닙니다. 안정 기준선이 충분하다는 근거도 중요한 결과입니다. 프리뷰 코드 양이 늘어날수록 매몰 비용 때문에 철회가 어려워지므로 실험 기한과 삭제 예산을 처음부터 둡니다.

프리뷰가 다음 JDK에서 안정이 되면 기존 클래스 파일을 그대로 써도 되나요?

아닙니다. 프리뷰 클래스 파일은 해당 릴리스에 종속됩니다. 새 JDK의 정식 명세에 맞춰 소스를 다시 컴파일하고 실행 검사도 수행해야 합니다. 플래그 제거만으로 끝내지 말고 API의 시그니처와 의미가 바뀌었는지 확인합니다.

연습 문제

상태가 안정이면 승격, 다음 프리뷰면 마이그레이션, 철회 상태면 삭제를 반환하세요. 알 수 없음 상태는 보류합니다.

해설 보기
exercise/PreviewDecisionSolution.java
public final class PreviewDecisionSolution {
    private enum Status {
        STABLE,
        REPREVIEW,
        WITHDRAWN,
        UNKNOWN
    }

    private enum Decision {
        PROMOTE,
        MIGRATE,
        DELETE,
        HOLD
    }

    static Decision decide(Status status) {
        return switch (status) {
            case STABLE -> Decision.PROMOTE;
            case REPREVIEW -> Decision.MIGRATE;
            case WITHDRAWN -> Decision.DELETE;
            case UNKNOWN -> Decision.HOLD;
        };
    }

    public static void main(String[] args) {
        System.out.println(decide(Status.STABLE));
        System.out.println(decide(Status.REPREVIEW));
        System.out.println(decide(Status.WITHDRAWN));
        System.out.println(decide(Status.UNKNOWN));
    }
}

종료 기준은 PROMOTE, MIGRATE, DELETE, HOLD 순서입니다. 실제 결정에는 동작 근거, 소유자, 기한, 안정 대체 경로 상태를 함께 기록합니다.