역사. Java 9 이전에는 반사가 setAccessible(true)를 통해 접근 제어자를 임의로 우회할 수 있어 캡슐화를 의도적으로 무너뜨릴 수 있었습니다. **Java 플랫폼 모듈 시스템 (JPMS)**의 도입은 기본적으로 강력한 캡슐화를 설정하였으며, 모듈은 내부 패키스에 대한 깊은 반사 접근 권한을 명시적으로 허가해야 합니다.
문제. 하나의 모듈의 코드가 다른 모듈의 패키지에 있는 비공식 필드에 접근하기 위해 MethodHandles 또는 기본 반사를 시도할 때, JVM은 철저한 접근성 검사를 수행합니다. 이 검증은 대상 패키지가 호출자의 모듈에 명시적으로 개방되었는지 확인합니다. 이 허가 없이는 JVM이 InaccessibleObjectException(또는 레거시 반사를 위한 IllegalAccessException)을 발생시키며, SecurityManager가 설치되어 있거나 필드가 VarHandle을 통해 접근되더라도 마찬가지입니다.
해결책. 모듈은 module-info.java에서 opens package.name [to specific.module];라고 선언해야 하거나 응용 프로그램은 --add-opens source.module/package.name=target.module 플래그로 실행되어야 합니다. 이 지시문은 모듈의 내부 접근성 그래프를 동적으로 수정하여 특정 내부 패키지의 비공개 멤버에 대해 깊은 반사를 수행할 수 있도록 허가된 모듈 집합에 대상 모듈을 추가합니다.
// 모듈: app.core (module-info.java) module app.core { // 패키지 com.app.internal은 열리지 않음 exports com.app.api; } // 모듈: framework.inject public class Injector { public void inject(Object target) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.privateLookupIn( target.getClass(), MethodHandles.lookup() ); // --add-opens 없이 InaccessibleObjectException 발생 VarHandle handle = lookup.findVarHandle( target.getClass(), "secretField", String.class ); handle.set(target, "injected"); } }
개발 팀은 그들의 단일체 Spring 기반 응용 프로그램을 Java 모듈 시스템으로 마이그레이션하면서 코드베이스를 핵심 비즈니스 논리 모듈(app.core)과 별도의 의존성 주입 프레임워크 모듈(framework.inject)로 분할하였습니다. 배포 직후, 프레임워크가 app.core의 내부 com.app.internal 패키지에 있는 비공식 필드에 구성 값을 주입하려고 시도할 때 InaccessibleObjectException이 발생하면서 애플리케이션이 중단되었습니다.
세 가지 잠재적 아키텍처 솔루션을 평가하였습니다. 첫 번째 접근법은 모든 주입 가능한 클래스를 app.core 내의 내보내진 패키지로 이동하는 것이었습니다. 이것은 즉각적인 접근 위반 문제를 해결할 수 있었지만, 내부 구현 세부 사항을 모든 다른 모듈에 노출시켜 캡슐화 원칙을 근본적으로 위반하게 되어 유지 관리 부담을 증가시키고 향후 보안 감사에 대한 공격 표면을 넓히게 됩니다. 두 번째 솔루션은 --add-exports JVM 인수를 사용하여 내부 패키지를 프레임워크 모듈에 노출시키는 것이었습니다. 하지만 --add-exports는 공용 유형에 대한 컴파일 및 런타임 가시성을 제공하지만, 비공식 멤버에 대한 깊은 반사를 명시적으로 허용하지 않기 때문에 Spring의 필드 주입 메커니즘에 필요했던 비공식 상태 수정에는 불충분했습니다. 세 번째 옵션은 --add-opens app.core/com.app.internal=framework.inject라는 대상을 사용했습니다. 이 접근법은 모든 다른 모듈에 대해 엄격한 소스 수준 캡슐화를 유지하며, 오직 주입 프레임워크에만 해당 특정 내부 패키지에 대한 깊은 반사를 수행할 수 있는 필요한 권한을 명시적으로 부여합니다.
팀은 최종적으로 세 번째 옵션을 선택하고 필요한 --add-opens 지시문을 배포 스크립트 및 Docker 구성에 문서화하였습니다. 이 솔루션은 개발 중 모듈 시스템의 무결성을 유지하면서 프레임워크가 올바르게 작동할 수 있도록 했으며 명시적으로 제어된 접근 경계를 통해 성공적인 마이그레이션을 이루었습니다.
왜 setAccessible(true)가 내보내진 패키지 내의 비공식 필드에 대해 다른 모듈에서 접근할 때 실패하는가, 보안 관리자가 없는데도 불구하고?
후보자들은 종종 패키지 내보내기를 개방성과 혼동합니다. exports 지시문은 공용 유형 및 멤버를 표준 컴파일 및 호출을 위해 접근 가능하게 만들 뿐, Java 언어 접근 확인을 억제하는 데 필요한 ReflectPermission을 부여하지 않습니다. JPMS의 강력한 캡슐화는 SecurityManager와 독립적으로 작동하며, JVM의 접근 제어 메커니즘에 의해 직접적으로 시행됩니다. 비공식 멤버에 대해 setAccessible(true)를 가능하게 하려면 패키지가 명시적으로 open으로 선언되거나 전체 모듈이 open module로 선언되어야 합니다.
MethodHandles.Lookup 캡처 메커니즘이 모듈 간 접근성에 어떻게 영향을 미치며, MethodHandles.lookup().in(targetClass)를 호출하는 것이 왜 조회의 능력을 저하시킬 수 있는가?
Lookup 객체는 생성자의 모듈 및 패키지 컨텍스트의 접근 권한을 캡슐화합니다. Lookup.in(targetClass)가 호출되면, JVM은 조회의 권한을 대상 클래스의 모듈에 따라 재평가합니다. 대상 클래스가 조회의 모듈에 패키지를 개방하지 않은 다른 모듈에 있을 경우, 조회는 PUBLIC 모드로 "저하"되어 PRIVATE 및 MODULE 접근 능력이 제거됩니다. 모듈 간에 전체 접근 권한을 유지하기 위해서는 대상 모듈이 조회의 모듈에 패키지를 명시적으로 개방해야 하며, 코드가 privateLookupIn을 사용해야 합니다. 이 경우에는 대상 클래스가 동일한 모듈 내에 있거나 모듈 그래프를 통해 접근 가능해야 합니다.
--add-exports와 --add-opens 간의 근본적인 차이는 무엇이며, 왜 전자가 의존성 주입 시 IllegalAccessException을 유발하게 되는가, 컴파일이 성공함에도 불구하고?
--add-exports 플래그는 패키지를 모듈의 내보내기 목록에 추가하여 대상 모듈이 컴파일 및 런타임 모두에서 공용 유형에 접근할 수 있도록 합니다. 그러나 이 지시문은 모듈의 "open" 세트를 수정하지 않으며, 이 세트는 깊은 반사를 제어합니다. Java 언어 명세는 가독성(내보내기)와 반사 가능성(열림)을 엄격히 분리합니다. 의존성 주입 프레임워크는 깊은 반사를 통해 비공식 필드를 조작하기 위해 후자를 필요로 합니다. 결과적으로, --add-exports가 컴파일러를 만족시켜 메서드 호출을 허용하더라도, 런타임에서 비공식 상태를 수정하려는 시도는 여전히 실패할 것입니다. 오직 --add-opens만이 깊은 반사를 위한 접근이 가능한 패키지 세트에 패키지를 추가하여 프레임워크가 비공식 필드 값을 변경할 수 있도록 허용합니다.