Spring Boot 2.5→3.2, Java 11→17 버전업 후 로그인 시 Gson이 LocalDateTime을 Redis에 직렬화하다 InaccessibleObjectException으로 터진 사고를, 현상 해결에 그치지 않고 Java 모듈 시스템(JEP)의 캡슐화 강화라는 근본 원인까지 파고든 글이다.
핵심 포인트- Gson은 커스텀 TypeAdapter가 없으면 ReflectiveTypeAdapterFactory가 리플렉션으로 private 필드에 setAccessible(true)를 걸어 직렬화하는데, Java 17에서 이 접근이 막혔다.
- 원인은 Java 9(Jigsaw) 도입 → Java 16(JEP 396, 기본 deny) → Java 17(JEP 403)로 이어진 캡슐화 강화로, java.base 같은 시스템 모듈은 add-opens로도 열기 어려워졌다.
- 필드 가시성을 높이는 건 캡슐화·보안상 나쁘므로, LocalDateTime 전용 Custom TypeAdapter를 만들어 리플렉션 경로를 우회했다.
- Gson 프로젝트도 java.* 내부 객체 직렬화는 역사적 부채라 근본 수정이 어렵다고 밝혀, 커스텀 어댑터가 사실상 정답이다.
상세 정리- 증상: 버전업 후 로그인에서 생성한 ValidToken 객체를 Redis에 직렬화 저장하는 과정에서 "Failed making field java.time.LocalDateTime#date accessible" 예외 발생.
- 1차 조치: 에러 메시지가 제안한 두 방법(가시성 상향 vs Custom TypeAdapter) 중 후자를 택해 LocalDateTimeAdapter를 등록해 즉시 해결.
- 근본 탐구: 스택 트레이스가 ReflectionHelper.makeAccessible과 ReflectiveTypeAdapterFactory.getBoundFields를 가리켰다.
- Gson 동작: getAdapter가 캐시를 보고 없으면 등록된 TypeAdapterFactory를 순회해 어댑터를 찾는데, 커스텀이 없으면 ReflectiveTypeAdapterFactory가 리플렉션으로 필드에 접근한다.
- Java 11 vs 17: 같은 코드가 11에선 되고 17에선 "module java.base does not opens java.time" 로그와 함께 실패했다.
- 모듈 시스템 변화: Java 9는 하위 호환 위해 relaxed strong encapsulation(illegal-access permit 기본)이었고, 16은 JEP 396으로 기본 deny, 17은 JEP 403으로 illegal-access 옵션 자체를 제거하고 시스템 모듈 add-opens를 제한했다.
- 결과적 원인: java.base 모듈의 private 필드에 대한 리플렉션 접근이 완전히 차단돼 Gson의 기본 직렬화 경로가 17에서 막혔다.
- 해결 원리: Custom TypeAdapter를 등록하면 TypeAdapterFactory 순회에서 ReflectiveTypeAdapterFactory보다 우선 적용돼 리플렉션을 아예 타지 않는다.
왜 읽나Java 11→17 / Spring Boot 3 마이그레이션에서 Gson·리플렉션 기반 라이브러리의 InaccessibleObjectException을 만난 백엔드 개발자에게 원인과 정석 해결을 알려주는 글.