프리뷰 기능 종료 전략
class 존재를 안정성으로 오판하는 반례를 고치고 JEP·JDK별 기능 목록·안정 기준선·어댑터·삭제·승격 기준으로 프리뷰 실험을 관리합니다.
프리뷰는 채택을 약속하지 않습니다.
다음 릴리스에서 같은 이름으로 다시 프리뷰되더라도 시그니처와 의미가 달라질 수 있습니다.
Java 25의 구조화된 동시성은 다섯 번째 프리뷰에서 open()과 Joiner 중심 API로 바뀌었고, Stable Values와 기본형 패턴도 후속 릴리스에서 계속 변화합니다.
소스를 유지하는 것보다 실험 질문과 관찰 근거를 유지해야 합니다.
클래스 존재와 안정성 오판
아래 확인 프로그램은 리플렉션으로 java.lang.StableValue를 찾고 존재하면 안정 기능이라고 출력합니다.
JDK 25에는 해당 클래스가 실제 존재하지만 프리뷰 API이므로 결과 wrong-stable=true는 잘못된 판단입니다.
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 릴리스, 상태, 프리뷰 차수와 재평가 날짜를 구조화합니다. 운영 기록에는 아래 예제에 없는 컴파일·실행 플래그, 소유자, 근거 링크도 필요합니다.
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()));
}
}목록의 날짜는 예시 입력이며 검토를 마쳤다는 증거가 아닙니다. 이 원문은 기능명·JEP·상태를 출력할 뿐 빌드 플래그를 자동 결정하지 않습니다.
OpenJDK JEP와 실제 설치된 JDK 컴파일 확인 프로그램을 함께 확인합니다.
안정 기준선·프리뷰 어댑터 비교
Stable Values를 평가할 때 기존 해법이 무엇인지 먼저 고정합니다.
synchronized 이중 검사, 홀더 관용구, 즉시 초기화한 final 필드처럼 정식 기능으로 만든 기준선이 요구를 충족한다면 프리뷰 도입의 이익이 작을 수 있습니다.
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"));
}
}다음 별도 프로그램은 작은 LazyCell 인터페이스 뒤에 프리뷰 구현을 둡니다. 앞의 기준선 클래스가 이 인터페이스를 구현하는 코드는 아닙니다.
public 도메인 API가 JDK 프리뷰 타입을 직접 반환하면 철회 시 소비자 전체가 바뀝니다.
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"));
}
}두 main은 정상 문자열의 직렬 호출만 보여 줍니다. 다음 정책 차이는 원문과 API 계약에 따른 것이며, 같은 동시성·실패 시나리오에서의 실행 비교는 별도로 필요합니다.
원문 코드와 Java 25 명세에 따른 조건과 결과를 비교합니다.
| 초기화 상황 | 정식 기능 기준선 | StableValue 실험 |
|---|---|---|
| 정상 문자열 반환 | 첫 값 저장 후 재사용 | 첫 값 저장 후 재사용 |
| null 반환 | 예외 · 미설정 유지 | null을 설정된 값으로 저장 |
| 공급자 예외 | 예외 전파 · 나중에 재시도 가능 | 예외 전파 · 나중에 재시도 가능 |
| 같은 셀 재귀 초기화 | 명시적인 재귀 방어 없음 | IllegalStateException |
- 정상 문자열 반환
- 정식 기능 기준선: 첫 값 저장 후 재사용StableValue 실험: 첫 값 저장 후 재사용
- null 반환
- 정식 기능 기준선: 예외 · 미설정 유지StableValue 실험: null을 설정된 값으로 저장
- 공급자 예외
- 정식 기능 기준선: 예외 전파 · 나중에 재시도 가능StableValue 실험: 예외 전파 · 나중에 재시도 가능
- 같은 셀 재귀 초기화
- 정식 기능 기준선: 명시적인 재귀 방어 없음StableValue 실험: IllegalStateException
API를 사용하는 코드가 짧다는 이유만으로 선택하지 말고 의미, 성능, 진단 측면의 이점을 측정합니다.
JDK 변경과 컴파일 재확인
새 JDK 도구 체인이 준비되면 이전 소스를 그대로 컴파일해 실패를 근거로 남긴 뒤 해당 릴리스의 JEP에 맞춰 실험 코드를 수정합니다.
하나의 소스를 조건부 리플렉션으로 여러 JDK에 억지로 맞추려 하지 않습니다.
다음 프로그램의 observed="compiled"는 직접 입력한 예시 문자열입니다. 컴파일러를 실행하거나 실제 검증 결과를 수집하지 않습니다.
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의 시그니처와 의미가 바뀌었는지 확인합니다.
연습 문제
상태가 안정이면 승격, 다음 프리뷰면 마이그레이션, 철회 상태면 삭제를 반환하세요.
알 수 없음 상태는 보류합니다.
해설 보기
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 순서입니다.
실제 결정에는 동작 근거, 소유자, 기한, 안정 대체 경로 상태를 함께 기록합니다.