- 들어가며 — 어느 날 stderr에 세 줄이 늘었다
- final은 지금까지 final이 아니었다
- JEP 500이 JDK 26에서 실제로 바꾼 것
- 새 플래그, 그리고 --add-opens로는 안 되는 이유
- 실제로 뭐가 걸리나 — 이슈 트래커가 말해 주는 것
- 지금 할 수 있는 것 — deny로 돌려보고, JFR로 범인 찾기
- 라이브러리 쪽 대안, 그리고 ReflectionFactory라는 반쪽짜리 탈출구
- 정직한 트레이드오프 — 그리고 지금 당장은 안 급한 이유
- 마치며
- 참고 자료
들어가며 — 어느 날 stderr에 세 줄이 늘었다
JDK 26으로 올리고 애플리케이션을 띄웠는데, 아무것도 안 바꿨는데 표준 에러에 이런 게 찍힙니다.
WARNING: Final field finalField in class TestFinalFieldModification$ClassWithFinalField has been mutated reflectively by class com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2 in unnamed module @59fd97a8 (file:/.../gson-2.13.2.jar)
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
이건 제가 지어낸 예시가 아니라, Gson 이슈 트래커에 실제로 올라온 재현입니다. 재현 코드는 놀랄 만큼 평범합니다 — final String 필드 하나 있는 클래스에 new Gson().fromJson(...)을 돌린 게 전부입니다. JDK 25에서는 조용히 잘 되던 코드가, JDK 26에서는 경고를 뱉습니다.
범인은 JEP 500: Prepare to Make Final Mean Final입니다. Ron Pressler와 Alex Buckley가 쓴 이 JEP는 2026년 3월 17일 GA한 JDK 26에 Closed/Delivered로 실렸습니다. 이름 그대로 "final이 진짜 final을 의미하게 만들기 위한 준비" 단계이고, 이번 릴리스에서 하는 일은 딱 하나 — 경고를 띄우는 것입니다.
final은 지금까지 final이 아니었다
JEP의 표현을 그대로 옮기면 이렇습니다. "final 필드가 재할당될 수 없다는 기대는 거짓이다(false)." 왜냐하면 JDK 5부터 리플렉션 API가 이걸 허용해 왔기 때문입니다.
// final 필드를 가진 평범한 클래스
class C {
final int x;
C() { x = 100; }
}
// 1. C의 final 필드에 딥 리플렉션
java.lang.reflect.Field f = C.class.getDeclaredField("x");
f.setAccessible(true); // C의 final 필드를 변경 가능하게
// 2. 인스턴스 생성
C obj = new C();
System.out.println(obj.x); // 100 출력
// 3. final 필드를 변경
f.set(obj, 200);
System.out.println(obj.x); // 200 출력
f.set(obj, 300);
System.out.println(obj.x); // 300 출력
이게 왜 들어갔냐면, 역직렬화 때문입니다. JDK 자신의 플랫폼 직렬화는 생성자를 우회해서 스트림의 값을 필드에 직접 꽂는데, 서드파티 직렬화 라이브러리도 동등한 기능을 제공하려면 같은 짓이 필요했습니다. 그래서 JDK 5에서 리플렉션 API를 바꿔 final 필드를 바꿀 수 있게 했습니다. JEP는 이 결정을 "돌이켜 보면 무결성을 희생한 나쁜 선택"이었다고 씁니다.
문제는 이게 두 가지를 동시에 망가뜨린다는 것입니다.
첫째, 정확성 추론이 불가능해집니다. final 필드의 불변성은 JDK 5부터 자바 메모리 모델에서 핵심 역할을 해 왔습니다 — 멀티스레드 코드에서 객체의 안전한 초기화(safe initialization)가 여기에 얹혀 있습니다. Lucene 이슈에 달린 한 코멘트가 이 지점을 정확히 찌릅니다. 리플렉션으로 final 필드를 바꾸는 건 "허용"돼 있었을 뿐, 일관되게 동작한 적이 없다는 것입니다 — 바꾼 값이 모든 스레드에서 보이지는 않더라는 관찰입니다. 즉 이건 JDK 26이 새로 만든 문제가 아니라, 원래부터 깨져 있던 걸 이제 말해 주기 시작한 것에 가깝습니다.
둘째, JIT가 최적화를 못 합니다. 대표적으로 상수 폴딩(constant folding)입니다. final 필드가 절대 안 바뀐다고 믿을 수 있어야 상수 표현식을 매번이 아니라 한 번만 평가하고, 거기서부터 최적화 연쇄가 시작됩니다. 그런데 "어떤 코드든 언제든 final 필드를 바꿀 수 있다"면 JVM은 모든 final 필드를 의심해야 합니다. JEP의 표현으로는, 그런 API가 존재한다는 사실만으로 모든 프로그램의 안전성과 성능이 함께 깎입니다. 실제로 final 필드를 바꾸는 코드는 극소수인데도 말입니다.
이 방향 자체는 갑자기 튀어나온 게 아닙니다. JDK 15의 히든 클래스와 JDK 16의 레코드는 처음부터 딥 리플렉션의 final 필드 변경을 막아 뒀고, JDK 17에서 JDK 내부를 강하게 캡슐화했고, JDK 24에서 sun.misc.Unsafe의 해당 메서드들을 제거하는 절차를 시작했습니다. JEP 500은 같은 "기본적으로 무결성(integrity by default)" 노선의 다음 칸입니다.
JEP 500이 JDK 26에서 실제로 바꾼 것
핵심은 Field::set의 동작 변경입니다. f.set(...)이 호출됐고 그 필드가 final이면, 이제 다음 세 조건이 모두 참이어야 변경됩니다.
f.setAccessible(true)가 이미 성공했고,- 필드를 선언한 클래스의 패키지가 호출자 모듈에 open돼 있고,
- 호출자 모듈에 대해 final 필드 변경이 활성화돼 있어야 합니다.
2번과 3번이 JDK 26에서 새로 생긴 조건입니다. 여기서 자주 오해가 생기는 지점 하나 — setAccessible의 동작은 바뀌지 않았습니다. 즉 f.setAccessible(true)는 여전히 성공하는데 f.set(...)은 불법일 수 있습니다. 둘은 이제 별개 관문입니다.
조건이 깨졌을 때 런타임이 무엇을 할지는 새 옵션 --illegal-final-field-mutation이 정합니다. JDK 9의 --illegal-access(JEP 261), JDK 24의 --illegal-native-access(JEP 472)와 같은 계보입니다 — JEP가 직접 이 둘을 선례로 듭니다.
| 값 | 동작 |
|---|---|
allow | 경고 없이 변경 허용 |
warn | 변경은 허용하되 경고. JDK 26의 기본값 |
debug | warn과 같되, 매 변경마다 경고 + 스택 트레이스 |
deny | Field::set이 IllegalAccessException을 던짐. 장래의 기본값 |
JEP가 명시한 로드맵은 이렇습니다 — 장래 릴리스에서 deny가 기본이 되고, 그때 allow는 제거되며, warn과 debug는 최소 한 릴리스 더 남습니다. warn 모드 자체도 결국 단계적으로 폐지됩니다.
경고 형식은 JEP에 이렇게 적혀 있습니다.
WARNING: Final field f in p.C has been [mutated/unreflected for mutation] by class com.foo.Bar.caller in module N (file:/path/to/foo.jar)
WARNING: Use --enable-final-field-mutation=N to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
여기에 함정이 있습니다. 경고는 리플렉션을 하는 모듈당 최대 한 번만 나옵니다. 애플리케이션 전체가 클래스패스에 있으면(대부분 그렇습니다) 전부 unnamed module 하나로 묶이니까, 위반이 열 군데든 백 군데든 경고는 딱 한 번 뜨고 끝입니다. 즉 이 경고를 보고 "우리는 한 군데만 걸렸네"라고 읽으면 안 됩니다. 세는 도구가 아니라 알람일 뿐입니다.
바뀐 건 Field::set만이 아닙니다. MethodHandles.Lookup::unreflectSetter도 같은 방식으로 바뀌었고, 런타임에 패키지를 여는 Module::addOpens 계열도 영향을 받습니다 — 커맨드라인에서 아무도 활성화되지 않은 패키지라면 JVM은 그 패키지의 final 필드를 신뢰하기로 하고, 이후 addOpens를 호출해도 final 필드 변경 권한은 생기지 않습니다.
반대로 안 바뀐 것도 못 박아 둡니다. System의 setIn/setOut/setErr는 JDK 26에서 아무것도 바뀌지 않았습니다. 이 필드들은 원래부터 write-protected라 딥 리플렉션으로 바꿀 수 없었고, 해당 메서드로만 바꿀 수 있었습니다.
새 플래그, 그리고 --add-opens로는 안 되는 이유
경고와 (장래의) 예외를 피하려면 새 영구 옵션 --enable-final-field-mutation을 씁니다.
# 클래스패스의 모든 코드에 대해 허용
$ java --enable-final-field-mutation=ALL-UNNAMED ...
# 모듈 경로의 특정 모듈에만 허용
$ java --enable-final-field-mutation=M1,M2 ...
런처에 직접 넘기는 것 말고도 경로가 몇 가지 더 있습니다 — JDK_JAVA_OPTIONS 환경 변수, java @config 같은 인자 파일, 실행 가능 JAR의 매니페스트에 Enable-Final-Field-Mutation 추가(단 지원되는 값은 ALL-UNNAMED 뿐이고, 다른 값을 넣으면 예외가 납니다), jlink의 --add-options, 그리고 JNI Invocation API로 JVM을 임베드하는 네이티브 애플리케이션. 다만 이 옵션은 부트 모듈 레이어만 대상으로 하며, 사용자 정의 레이어에는 적용할 수 없습니다.
여기서 실무자가 제일 많이 헛발질할 지점을 짚습니다. --add-opens만으로는 이 경고가 사라지지 않습니다. JEP가 명시적으로 못 박은 문장입니다. 그리고 그 역도 참입니다 — --enable-final-field-mutation을 켠다고 해서 딥 리플렉션이 무조건 되는 게 아니고, 바꾸려는 final 필드가 그 코드에 open돼 있어야 합니다. 앞서 본 조건 2번과 3번은 AND입니다. 그래서 실무에서는 이 둘을 같이 쓰게 되는 경우가 많습니다.
또 하나 — 이 트레이드오프를 결정하는 주체는 애플리케이션 개발자(또는 그 조언을 받은 배포자)이지 라이브러리 개발자가 아닙니다. 이건 JEP가 정책적으로 못 박은 부분입니다. 라이브러리가 자기 사용자에게 "이 플래그 켜세요"라고 요구하는 건 최후의 수단이어야 합니다. 왜냐하면 플래그는 그 라이브러리에만 켜지는 게 아니라 모듈 단위로 켜지고, 클래스패스라면 결국 애플리케이션 전체에 켜지니까요.
실제로 뭐가 걸리나 — 이슈 트래커가 말해 주는 것
추측 대신 GA 전후로 실제로 올라온 이슈를 보겠습니다.
Gson. 앞서 본 이슈 #2991입니다. deny로 돌리면 경고가 아니라 예외로 바뀝니다. 흥미로운 건 메인테이너의 답변입니다 — final 필드를 바꾸는 게 Gson에 "깊이 배어 있어서(deeply ingrained)" 당장 할 수 있는 최선은 예외 메시지를 JEP 500에 맞게 고치고 대안을 문서화하는 것 정도라고 했습니다. 제시된 대안은 필드에서 final 떼기, 레코드 쓰기, 코틀린 data class + 전용 TypeAdapterFactory, 문제되는 클래스마다 커스텀 TypeAdapter나 JsonDeserializer 쓰기입니다. 덧붙여 리플렉션 기반 직렬화는 이 문제와 별개로도 나쁘다고 말합니다 — 클래스의 private 필드로부터 공개 JSON API가 유도되고, 그것도 Gson용으로 설계된 클래스가 아니라 아무 클래스에나 그렇게 된다는 이유입니다. 이 글을 쓰는 시점에 이슈는 열려 있고, 올라온 PR도 예외 메시지 개선 범위입니다.
Spring Security. 이슈 #19127입니다. spring-security-acl 7.0.5에서 BasicLookupStrategy가 AclImpl의 final 필드 aces를 리플렉션으로 바꿉니다. 딱 위 경고 세 줄이 그대로 찍힙니다.
Trino. 이슈 #28207이 가장 교훈적입니다. 테스트를 아예 --illegal-final-field-mutation=deny로 돌려 본 결과인데, 걸린 게 Trino 자기 코드가 아닙니다. picocli가 ClientOptions의 public final 리스트 필드에 값을 못 넣고, Jackson databind의 FieldProperty가 다른 라이브러리(hoverfly)의 final 필드를 못 씁니다. 에러 메시지가 아주 직설적입니다.
class com.fasterxml.jackson.databind.deser.impl.FieldProperty (in unnamed module @359df09a)
cannot set final field io.specto.hoverfly.junit.api.view.HoverflyInfoView.upstreamProxy
(in unnamed module @359df09a), unnamed module @359df09a is not allowed to mutate final fields
교훈은 이겁니다 — 이건 대체로 당신 코드의 문제가 아니라 의존성의 문제입니다. 그리고 당신이 고칠 수 없는 코드에서 납니다.
Lucene. 이슈 #15482는 반대편 사례입니다. Lucene은 forbiddenapis로 자기 코드의 setAccessible을 원래부터 금지해 왔기 때문에, 이제 테스트 러너에 deny를 넘겨서 의존성이 하는 짓까지 잡겠다는 계획입니다. 자기 집이 깨끗하면 이 플래그는 부담이 아니라 무기가 됩니다.
지금 할 수 있는 것 — deny로 돌려보고, JFR로 범인 찾기
JEP의 권고는 명확합니다. "장래를 대비하려면 기존 코드를 deny 모드로 돌려서 딥 리플렉션으로 final 필드를 바꾸는 코드를 찾아내라."
경고가 모듈당 한 번뿐이라 세기 어렵다는 문제는 두 가지 방법으로 우회합니다.
# 1) 매 변경마다 경고 + 스택 트레이스
$ java --illegal-final-field-mutation=debug -jar app.jar
# 2) JFR로 이벤트 수집 (JFR이 켜져 있으면 자동 기록)
$ java -XX:StartFlightRecording:filename=recording.jfr ...
$ jfr print --events jdk.FinalFieldMutation recording.jfr
jdk.FinalFieldMutation 이벤트는 final 인스턴스 필드를 바꾸거나 Lookup.unreflectSetter로 쓰기 가능한 MethodHandle을 얻을 때 기록되고, 필드를 선언한 클래스, 필드 이름, 그리고 스택 트레이스를 담습니다. 스택 트레이스가 있다는 게 중요합니다 — 위 Gson 사례처럼 실제 호출자가 프레임워크 깊숙한 곳일 때 이게 없으면 못 찾습니다.
실용적인 순서는 이렇게 됩니다.
- CI의 테스트 잡을 JDK 26 +
--illegal-final-field-mutation=deny로 한 번 돌려서 목록을 뽑는다(Trino와 Lucene이 정확히 이걸 합니다). - 나온 항목을 "내 코드 / 내가 고칠 수 있는 의존성 / 못 고치는 의존성"으로 나눈다.
- 내 코드는 고친다 — 대개 생성자로 옮기면 끝납니다.
- 못 고치는 것만 남겨서, 프로덕션 런타임에는
--enable-final-field-mutation으로 명시적으로 열어 둔다. 이건 도망이 아니라 기록입니다. 어디에 빚이 있는지가 커맨드라인에 남습니다.
라이브러리 쪽 대안, 그리고 ReflectionFactory라는 반쪽짜리 탈출구
JEP와 Avoiding Final Field Mutation이 제시하는 대안은 결국 하나로 수렴합니다 — 생성자를 쓰라는 것입니다.
문제의 뿌리는 "일단 빈 객체를 만들고 나중에 필드를 채우는(construct-first-assign-later)" 패턴입니다. 플랫폼 역직렬화가 그렇고, 자바빈이 그렇고, 예전 DI가 그랬습니다. 이 패턴은 final과 정면충돌하니까 강제 할당이 필요해집니다. 그런데 이건 final 문제 이전에 그 자체로 나쁩니다 — 클래스의 불변식은 보통 생성자에서 세워지는데, 생성자를 우회하면 불변식을 만족하지 않는 인스턴스가 만들어질 수 있습니다. Inside Java 글의 ReflectionFactory 예제가 이걸 대놓고 보여 줍니다. 생성자를 우회하니까 검증도 우회되고, age에 -5가 그냥 들어갑니다.
DI 진영은 이미 이 길을 걸었습니다. 필드 주입에서 생성자 주입으로 옮겨 왔고, 대부분의 프레임워크가 이제 final 필드 주입을 금지하거나 권장하지 않습니다. 직렬화 라이브러리라면 레코드로 한정하기, 레코드를 프록시로 쓰기, 또는 사용자가 생성자/정적 팩터리를 지정하게 하기 같은 선택지가 있습니다. 클론이라면 super.clone() 대신 복사 생성자나 정적 팩터리로 갑니다(Effective Java의 오래된 조언이 그대로 유효합니다).
그럼 기존 직렬화 라이브러리는? JEP의 공식 답은 sun.reflect.ReflectionFactory입니다. jdk.unsupported 모듈에 있는 critical internal API이고, 여기서 얻은 메서드 핸들은 JDK 자신의 직렬화와 같은 권한을 갖습니다. 커맨드라인 플래그가 필요 없고, deny에서도 경고나 에러가 나지 않습니다.
그런데 여기에 결정적인 제약이 있습니다. ReflectionFactory는 java.io.Serializable을 구현한 클래스의 역직렬화만 지원합니다. 이건 버그가 아니라 의도된 선을 긋는 것입니다. JEP의 설명이 명쾌합니다 — JVM은 Serializable 객체의 final 필드는 변경될 수 있다고 가정해야 하지만, 그 외 모든 객체(압도적 다수)의 final 필드는 영구히 불변이라고 가정할 수 있게 하려는 것입니다. 상수 폴딩 같은 최적화의 여지를 지키기 위한 거래입니다.
그래서 이 탈출구는 Gson에게 별 도움이 안 됩니다. Gson은 Serializable 구현을 요구하지 않으니까요. 이슈에서 지적된 그대로입니다. 메인테이너의 반응도 시니컬하게 정확합니다 — 값 클래스에 Serializable을 붙이도록 고칠 거면, 차라리 레코드로 만드는 게 낫지 않냐는 것입니다. Inside Java 글도 같은 경고를 답니다. ReflectionFactory는 "겁 많은 사람을 위한 물건이 아닌" 날카로운 도구이고, 플랫폼 직렬화의 온갖 난점(readObject/readResolve 프로토콜 재구현, 보안, Serializable 확산)을 그대로 떠안습니다.
정직한 트레이드오프 — 그리고 지금 당장은 안 급한 이유
여기서 톤을 낮춰야 할 부분들입니다.
성능 이득에 숫자가 없습니다. JEP는 "더 안전하고 잠재적으로 더 빠르다(potentially faster)"고 씁니다. Oracle의 JDK 26 주요 변경 문서도 같은 표현을 씁니다. 메커니즘(상수 폴딩과 그 뒤의 최적화 연쇄)은 설명하지만, "몇 % 빨라진다"는 벤치마크는 어디에도 없습니다. 이 글에서도 지어내지 않겠습니다. 지금 시점에 정직한 요약은 "미래의 최적화를 위한 여지를 확보하는 변경이고, 그 이득은 아직 측정치로 제시되지 않았다"입니다.
왜 투기적 최적화로 안 되냐는 반론에 JEP가 직접 답한 부분은 오히려 흥미롭습니다. JIT가 늘 하듯 "final은 안 바뀐다고 낙관적으로 가정하고, 바뀌면 역최적화(deoptimize)"하면 되지 않냐는 것인데, JEP의 답은 그걸로는 부족할 수 있다는 것입니다. 앞으로 계획된 최적화들은 프로세스 수명 안에서의 불변성만이 아니라 실행에서 다음 실행으로 넘어가는 불변성에 기댈 수 있기 때문입니다. AOT 캐시와 Leyden 계열 작업을 떠올리면 이 문장의 무게가 읽힙니다. 런타임에 역최적화할 수 있는 것과, 이전 실행에서 구워 둔 걸 믿는 것은 다른 문제입니다.
그리고 타임라인. 이게 실무자에게 제일 중요합니다. JEP 500은 deny가 기본이 될 릴리스를 명시하지 않습니다. 그냥 "장래 릴리스"입니다. 그리고 2026년 9월 15일 GA 예정인 JDK 27은 이미 Rampdown Phase One을 지나 기능 세트가 동결됐고("No further JEPs will be targeted to this release"), 확정된 JEP 목록에 final 필드 강제와 관련된 건 없습니다. 즉 JDK 27에서 이게 에러가 되지는 않습니다. 빨라도 그다음입니다.
그래서 지금 이 경고에 대한 합리적인 반응은 이렇습니다.
신경 써야 하는 경우
- JDK 26 이상으로 올렸고, 스택에 리플렉션 기반 직렬화(Gson 등)나 오래된 필드 주입이 있다.
- 라이브러리·프레임워크 메인테이너다. 여기는 진짜 숙제입니다 — 사용자에게 플래그를 요구하는 건 최후의 수단이라는 게 JEP의 입장이고, 아키텍처를 바꾸는 데는 시간이 걸립니다.
- 테스트가 이미
deny로 돌 만큼 집이 깨끗하다면, 지금 켜서 회귀를 막는 게 이득입니다.
아직 신경 안 써도 되는 경우
- 아직 JDK 21이나 25 LTS에 있다. 이 변경은 JDK 26부터이고, JDK 26은 6개월 주기의 비-LTS 릴리스입니다. 대다수 팀은 다음 LTS를 통해 이걸 처음 만나게 됩니다.
- 경고가 뜨는 게 전부이고, 그 원인이 남이 만든 라이브러리이며, 당신은 지금 다른 불을 끄는 중이다.
--enable-final-field-mutation=ALL-UNNAMED로 조용히 시키고 백로그에 적어 두는 건 완전히 합당한 선택입니다. 이건 영구 옵션이지 임시 유예가 아닙니다.
다만 반사적으로 --illegal-final-field-mutation=allow를 박아 넣는 건 피하는 게 좋습니다. JEP가 명시적으로 말합니다 — deny가 기본이 되는 날 allow는 제거됩니다(warn과 debug는 최소 한 릴리스 더 남습니다). 즉 유효기간이 붙은 값이고, 그날 이 플래그를 쥔 팀은 어떻게든 다시 손대야 합니다. 굳이 지금 조용히 시켜야 한다면 --enable-final-field-mutation 쪽이 맞습니다. 이쪽은 없어질 예정이 없는 영구 옵션이고, 무엇보다 "우리는 여기서 final을 바꾸고 있다"를 커맨드라인에 남긴다는 점에서 정직합니다.
마지막으로 사각지대 하나. 이 모든 진단은 자바 코드 얘기입니다. 네이티브 코드가 JNI의 Set<type>Field 계열로 final 필드를 바꾸는 건 정의되지 않은 동작(undefined behavior)이고, JEP는 그 결과로 배열 경계 검사가 무너지거나 프로세스가 죽을 수도 있다고까지 씁니다. 진단은 -Xlog:jni=debug나 -Xcheck:jni로만 얻을 수 있습니다. 그리고 sun.misc.Unsafe로 바꾸는 건 진단조차 없습니다. 조용히 이상한 버그나 JVM 크래시로 나타날 수 있습니다.
마치며
정리하면 이렇습니다. JDK 26은 딥 리플렉션의 final 필드 변경에 경고를 띄우기 시작했습니다. 동작은 아직 그대로이고, 기본값은 warn이며, 경고는 모듈당 한 번뿐이라 실제 규모를 알려면 deny나 JFR이 필요합니다. 장래에 deny가 기본이 되지만 그게 언제인지는 JEP도 말하지 않았고, 최소한 JDK 27은 아닙니다.
이 변경의 진짜 메시지는 플래그가 아니라 그 아래에 있습니다. 자바는 20년 넘게 "final은 불변이다"라고 말하면서 그걸 깨는 도구를 같이 쥐여 줬고, 그 위에 생태계의 상당 부분이 올라앉았습니다. JEP 500은 그 청구서를 이제 나눠서 발행하기 시작한 것입니다. 경고 한 줄은 작지만, 그 뒤에 있는 질문은 작지 않습니다 — 당신의 객체는 생성자로 완성됩니까, 아니면 누군가 나중에 필드를 채워 넣습니까.
참고 자료
- JEP 500: Prepare to Make Final Mean Final (Ron Pressler, Alex Buckley — JDK 26, Closed/Delivered)
- JDK 26 — 기능 목록과 GA 일정(2026/03/17)
- JDK 27 — Rampdown Phase One, 확정 JEP 목록(2026/09/15 GA 예정)
- Quality Outreach Heads-up — JDK 26: Warnings About Final Field Mutation (Nicolai Parlog, 2026/05/15)
- Avoiding Final Field Mutation (Nicolai Parlog, 2026/04/27) — 시나리오별 대안과 ReflectionFactory 예제
- Significant Changes in JDK 26 Release — Oracle 마이그레이션 문서
- google/gson #2991 — Prepare for final field mutation warning under Java 26
- spring-projects/spring-security #19127 — Final field mutation reported on Java 26
- trinodb/trino #28207 — illegal-final-field-mutation violations (deny로 테스트 실행)
- apache/lucene #15482 — When running tests on Java 26 disallow modification of final fields
현재 단락 (1/106)
JDK 26으로 올리고 애플리케이션을 띄웠는데, 아무것도 안 바꿨는데 표준 에러에 이런 게 찍힙니다.