pile·
백엔드·마켓컬리마켓컬리 Hello World·

Spring Boot 버전업 중 알게된 Java 버전별 캡슐화 정책 강화

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을 만난 백엔드 개발자에게 원인과 정석 해결을 알려주는 글.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2