Java 26 JEP 500, 경고 무시하면 다음 버전에서 터집니다

Published on

in

Java 26 JEP 500, 경고 무시하면 다음 버전에서 터집니다

2026.03.17 GA 기준
Java 26 (JDK 26)

Java 26 JEP 500, 경고 무시하면 다음 버전에서 터집니다

Java 26가 2026년 3월 17일 GA 출시됐습니다. 10개 JEP 중 지금 당장 실무에 불을 켜야 할 건 JEP 500 하나입니다. final 필드를 리플렉션으로 건드리면 이제 경고가 뜨고, 다음 버전에선 예외가 터집니다. Hibernate·Mockito·Lombok이 조용히 영향권에 들어왔습니다.

10개
JEP 포함
5~15%
G1 GC 처리량 향상
JDK 5
이래 열려 있던 구멍

Java 26, 지금 쓰는 코드에 바로 영향이 옵니다

Java 26 (JDK 26)이 2026년 3월 17일 GA 출시됐습니다. 오라클 공식 발표 기준 총 10개의 JEP가 포함됐는데, 이 중 지금 당장 로컬 빌드와 CI 로그에 흔적을 남기는 건 JEP 500 — Prepare to Make Final Mean Final입니다. (출처: Oracle 공식 발표, 2026.03.17)

이름만 보면 “언젠가 정비하겠다는 예고편 아닌가”라고 넘어가기 쉽습니다. 막상 Java 26으로 빌드를 돌려보면 다릅니다. 리플렉션으로 final 필드를 수정하는 코드가 프로젝트 어딘가에 있으면, 표준 에러 스트림에 경고가 쌓이기 시작합니다. 이 경고가 다음 릴리스에서 IllegalAccessException으로 격상된다고 JEP 500 공식 문서에 명시돼 있습니다.

비-LTS 릴리스라 “6개월 쓰고 버린다”는 논리가 통용되지만, JEP 500은 LTS 여부와 무관합니다. Java 25 LTS 기반 프로덕션을 유지하면서도 Java 26을 CI에 추가해 경고를 미리 잡아야 하는 이유가 여기 있습니다.

▲ 목차로 돌아가기

final이 21년째 진짜 final이 아니었습니다

JEP 500이 만들어진 배경부터 짚어야 충격이 제대로 전달됩니다. final이라는 키워드가 진짜로 불변을 보장하지 못해 왔다는 사실입니다.

💡 공식 JEP 문서와 실제 런타임 동작을 나란히 놓고 보면 이런 간극이 보입니다

JDK 5에서 리플렉션 API가 Field::setAccessible(true)Field::set()을 통해 final 필드 수정을 허용하도록 변경됐습니다. 당시 이유는 서드파티 직렬화 라이브러리가 역직렬화 시 객체를 초기화하기 위해 final 필드를 건드려야 했기 때문입니다. (출처: JEP 500 Motivation, openjdk.org)

다시 말해 2005년부터 지금까지, 아래 코드는 JVM이 아무 불평 없이 실행했습니다.

class C {
final int x;
C() { x = 100; }
}
Field f = C.class.getDeclaredField("x");
f.setAccessible(true);
C obj = new C();
f.set(obj, 200);  // final 필드를 200으로 교체 — JDK 5~25까지 정상 동작
f.set(obj, 300);  // 300으로 또 교체 — 역시 문제없음

결과적으로 JVM은 final 필드가 진짜로 불변이라고 가정하고 상수 폴딩(constant folding) 최적화를 적용해 왔습니다. 그런데 실제로는 런타임에 값이 바뀔 수 있으므로, 이 최적화가 무의미해지거나 잘못된 결과를 낼 수 있었습니다. JVM이 성능 최적화를 위해 믿어야 할 불변성이 사실 보장되지 않은 상태였습니다.

JEP 500은 이 구멍을 단계적으로 막겠다는 선언입니다. Java 26에서는 경고, 이후 버전에서는 IllegalAccessException 예외가 던져집니다. 예외 격상 타이밍은 오라클이 공식 답변을 내놓지 않은 부분입니다.

▲ 목차로 돌아가기

Hibernate·Mockito·Lombok, 지금 어떤 상태인가

문제는 내 코드가 아닐 수 있습니다. 의존성 라이브러리가 내부적으로 final 필드를 리플렉션으로 건드리고 있으면, 내가 아무것도 바꾸지 않았어도 Java 26 로그에 경고가 뜹니다.

실무에서 흔히 보이는 영향권 라이브러리는 다음과 같습니다.

라이브러리 영향 여부 비고
Spring Framework 7.0 ✅ 정리 완료 리플렉션 사용 정비 완료
Hibernate 7.x ⚠️ 일부 설정 영향 특정 설정값에 따라 경고 발생
Mockito (spy 기능) ❌ 경고 발생 spy 메커니즘이 final 필드 수정
Lombok ❌ 경고 발생 일부 어노테이션 처리 시
Jackson (기본 설정) ⚠️ 설정 의존 역직렬화 방식에 따라 상이

(출처: dev.to/paszekdev, Java 26 Spring Boot 분석, 2026.03.17 / JEP 500 공식 문서 openjdk.org)

Spring Framework 7.0은 이미 리플렉션 사용을 정비했습니다. 프레임워크 자체는 안전합니다. 문제는 테스트 코드에서 Mockito의 spy()를 쓰거나, Lombok의 특정 어노테이션 조합을 사용하는 경우입니다. 프로덕션 코드에서는 경고가 없더라도 테스트 실행 시 로그가 넘칩니다.

▲ 목차로 돌아가기

–add-opens로 막을 수 없는 이유가 있습니다

💡 기존 JEP들에서 쌓아온 우회 경험을 그대로 가져다 쓰면 이번엔 막히는 지점이 있습니다

JEP 403(JDK 17), JEP 472(JDK 24) 등 이전 JEP들은 --add-opens로 어느 정도 우회가 됐습니다. JEP 500은 다릅니다. --add-opens로 패키지를 열어도 final 필드 수정 경고는 사라지지 않습니다. (출처: JEP 500 Description, openjdk.org)

JEP 500 공식 문서에 정확히 이렇게 나와 있습니다: “It will not be possible to avoid the warning simply by using –add-opens to enable the deep reflection of classes with final fields.” — 기존 패턴이 통하지 않습니다.

경고를 없애려면 두 가지 방법 중 하나를 써야 합니다. 첫 번째는 --enable-final-field-mutation=ALL-UNNAMED 플래그를 JVM 시작 옵션에 추가하는 것입니다. 이건 임시방편이고, 예외 격상 이후에도 계속 먹힐지는 보장이 없습니다. 두 번째는 문제 라이브러리의 Java 26 호환 버전을 찾아 업그레이드하는 겁니다.

# Java 26에서 경고를 예외처럼 취급 — CI에서 지금 써보기
java --illegal-final-field-mutation=deny ...
# 라이브러리 업데이트 전 임시 억제 (미래 보장 없음)
java --enable-final-field-mutation=ALL-UNNAMED ...
# 어디서 터지는지 정확히 찍어보기
java -XX:StartFlightRecording:filename=recording.jfr ...
jfr print --events jdk.FinalFieldMutation recording.jfr

JFR(Java Flight Recorder)로 jdk.FinalFieldMutation 이벤트를 기록하면 어떤 클래스의 어떤 final 필드가 수정됐는지, 어느 스택에서 호출됐는지 정확히 확인됩니다. CI에서 --illegal-final-field-mutation=deny를 켜고 빌드해 두면, 미래 버전에서 실제로 예외가 발생하기 전에 문제를 잡을 수 있습니다.

▲ 목차로 돌아가기

G1 GC 성능 향상, 아무것도 안 해도 됩니다

JEP 500이 “해야 할 일”이라면, JEP 522는 “공짜로 받는 선물”입니다. G1 GC의 카드 테이블 방식이 바뀌었습니다.

기존에는 애플리케이션 스레드와 GC 스레드가 단일 카드 테이블을 공유하면서 쓰기 배리어마다 동기화 비용이 발생했습니다. Java 26의 JEP 522는 이를 듀얼 카드 테이블로 분리해서 동기화 오버헤드를 없앴습니다. 결과가 공식 발표 기준 처리량 5~15% 향상입니다. (출처: Oracle Java 26 공식 발표, 2026.03.17)

5~15%라는 수치는 “레퍼런스 많은 워크로드”가 기준입니다. HTTP 요청마다 객체를 생성하고, JPA 엔티티를 로드하고, 서비스 레이어에서 DTO를 만드는 일반적인 Spring Boot 앱이 여기 해당합니다. G1이 기본 GC이므로 JDK를 26으로 바꾸기만 해도 이 이득은 따라옵니다.

Java 25에서 이미 적용된 컴팩트 오브젝트 헤더(Compact Object Headers)는 오브젝트 헤더 크기를 12~16바이트에서 8바이트로 줄여 힙 사용량을 약 22% 낮췄습니다. (출처: JDK 25 릴리스 노트) 두 개를 합치면 메모리 압박이 심한 서비스에서 체감할 수 있는 수준의 이득입니다.

▲ 목차로 돌아가기

UUID v7이 표준 라이브러리에 들어왔습니다

JEP에 없지만 실무 영향이 작지 않은 변경이 하나 있습니다. UUID.ofEpochMillis(long timestamp) 메서드가 추가됐습니다.

UUID.randomUUID()는 v4로 완전 랜덤입니다. DB 기본 키로 쓰면 INSERT마다 B-Tree 인덱스의 임의 위치에 레코드가 삽입되면서 인덱스 단편화가 쌓입니다. UUID.ofEpochMillis()는 앞부분에 밀리초 타임스탬프가 붙어서 새 레코드가 항상 인덱스 끝에 붙습니다. 외부 라이브러리 없이 메서드 하나로 인덱스 단편화 문제를 해소할 수 있습니다.

💡 JEP 500과 UUID v7을 같이 놓고 보면 한 가지 흐름이 보입니다

둘 다 “지금껏 서드파티나 꼼수로 해결해 온 것을 표준화”하는 방향입니다. final 보장은 JVM 레벨에서, 시간순 UUID는 표준 API로 — 외부 의존성을 줄이는 쪽으로 언어가 움직이고 있습니다.

// 기존 — 랜덤 UUID, DB 인덱스 단편화 유발
UUID id = UUID.randomUUID();
// Java 26 — 시간순 UUID, 인덱스 끝에 순서대로 삽입
UUID id = UUID.ofEpochMillis(System.currentTimeMillis());

단, UUID.ofEpochMillis()는 밀리초 정밀도입니다. 같은 밀리초 내에서는 여전히 랜덤입니다. 초당 수천 건 이상 INSERT가 발생하는 극단적 부하에서는 충돌 가능성을 감안해야 합니다.

▲ 목차로 돌아가기

JEP 500 실무 대응 3단계

정리하면 이렇게 됩니다.

1단계

CI 매트릭스에 Java 26 추가

프로덕션은 Java 25 LTS를 유지하고, CI에서만 Java 26으로 빌드를 돌립니다. --illegal-final-field-mutation=deny 플래그를 추가해 경고가 아닌 오류로 만들어 두면 미래 버전 대비가 됩니다.

2단계

로그에서 “has been mutated reflectively” 검색

내 코드에서 나오는 경고인지 라이브러리에서 나오는 경고인지 구분합니다. JFR로 jdk.FinalFieldMutation 이벤트를 덤프하면 스택 트레이스까지 확인됩니다.

3단계

라이브러리별 대응

내 코드라면 생성자 주입 방식으로 리팩터링합니다. 라이브러리라면 Java 26 호환 버전 여부를 확인하고, 임시로 --enable-final-field-mutation=ALL-UNNAMED를 추가합니다. 직렬화 라이브러리는 sun.reflect.ReflectionFactory API로 전환할 것을 JEP 500이 권장합니다.

Java 26은 비-LTS라 9월에 지원이 끝납니다. 그 전에 경고 목록을 클리어해 두면, 이후 Java 27 또는 다음 LTS 버전으로 올라갈 때 JEP 500 관련 예외로 배포가 막히는 상황을 피할 수 있습니다.

▲ 목차로 돌아가기

Q&A

Java 26은 비-LTS인데, 지금 업그레이드해야 하나요?

프로덕션 업그레이드는 불필요합니다. Java 25 LTS가 있으므로 프로덕션은 그대로 유지하고, CI 파이프라인에만 Java 26 빌드를 추가해 JEP 500 경고를 미리 파악하는 용도로 활용하는 게 현실적입니다.

JEP 500 경고가 예외로 바뀌는 시점은 언제인가요?

오라클이 공식 타이밍을 공개하지 않은 부분입니다. JEP 500 문서에는 “a future JDK release”라고만 나와 있습니다. Java 27(2026.09 예정)이 될 수도 있고, 그 이후가 될 수도 있습니다. 정확한 버전이 나오기 전에 경고를 해소해 두는 편이 안전합니다.

직렬화 라이브러리는 예외 없이 영향을 받나요?

JEP 500은 직렬화 라이브러리에 대해 sun.reflect.ReflectionFactory API를 사용하라고 권고합니다. 이 API는 JDK 내부 직렬화와 동일한 방식으로 final 필드에 접근하며, 라이브러리가 이 API로 전환하면 --enable-final-field-mutation 플래그 없이도 동작합니다. Serializable 구현 클래스에만 적용됩니다.

G1 GC 성능 향상은 모든 JVM에 적용되나요?

G1이 기본 GC인 JDK 9+ 환경에서 적용됩니다. ZGC나 Shenandoah를 명시적으로 사용 중이라면 JEP 522의 직접적인 이득은 없습니다. 다만 JEP 516(AOT 오브젝트 캐싱)은 모든 GC에서 동작합니다.

Structured Concurrency는 언제 정식 기능이 되나요?

Java 26 기준 여섯 번째 미리보기(sixth preview)입니다. Java 19부터 미리보기로 들어왔는데 아직 정식 졸업을 못 했습니다. 이번 사이클에서 onTimeout()이 추가됐지만 정식화 일정은 오라클이 공개하지 않은 상태입니다. 당장 프로덕션에 쓰기엔 시기상조입니다.

▲ 목차로 돌아가기

마치며

Java 26을 비-LTS라는 이유로 넘기기 쉽습니다. 하지만 JEP 500은 버전 채택 여부와 무관하게 현재 코드베이스에 숨어 있는 문제를 드러냅니다. 경고가 언제 예외로 바뀔지 모르는 상황에서 “나중에 보자”는 선택은 다음 LTS로 올라갈 때 갑작스러운 배포 장애로 돌아올 수 있습니다.

JDK 5부터 21년 동안 열려 있던 구멍이 이제 닫히고 있습니다. G1 GC 성능 향상이나 HTTP/3 지원보다 체감하기 덜 화려하지만, 코드 정확성과 JVM 최적화 효율에 가장 직접적인 영향을 주는 변화입니다. CI에 Java 26 빌드를 하나 추가하는 것이 지금 당장 할 수 있는 가장 작은 투자입니다.

▲ 목차로 돌아가기

본 포스팅 참고 자료

  1. Oracle 공식 Java 26 발표 (한국어) — oracle.com/kr (2026.03.17)
  2. JEP 500 공식 문서 — openjdk.org/jeps/500
  3. Oracle Java 기술 블로그 — The Arrival of Java 26 — blogs.oracle.com/java (2026.03.17)
  4. Spring Boot Developer 관점 Java 26 분석 — dev.to/paszekdev (2026.03.17)

본 포스팅은 2026년 3월 25일 기준 Java 26 (JDK 26) GA 출시 시점의 정보를 바탕으로 작성됐습니다. 본 포스팅 작성 이후 서비스 정책·UI·기능이 변경될 수 있습니다. JEP 500의 경고→예외 격상 타이밍은 오라클이 별도 발표 전까지 변경 가능성이 있으며, 반드시 공식 릴리스 노트를 함께 확인하시기 바랍니다.

댓글 남기기


최신 글


아이테크 어른경제에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기