1. 개요

Java 9 이전에는 Java Reflection API에 강력한 기능이 있었습니다. 비공개 클래스 멤버에 제한 없이 액세스할 수 있었습니다. Java 9 이후 모듈식 시스템은 Reflection API를 합리적인 범위로 제한하려고 합니다.

이 사용방법(예제)에서는 모듈 시스템과 리플렉션 간의 관계를 검사합니다.

2. 모듈식 시스템과 반사

리플렉션과 모듈 시스템은 Java 역사에서 서로 다른 시기에 등장했지만 신뢰할 수 있는 플랫폼을 구축하려면 함께 작동해야 합니다.

2.1. 기본 모델

Java 모듈 시스템의 목표 중 하나는 강력한 캡슐화입니다. 강력한 캡슐화는 주로 가독성과 접근성으로 구성됩니다 .

  • 모듈의 가독성은 대략적인 개념이며 한 모듈이 다른 모듈에 종속되는지 여부와 관련됩니다.
  • 모듈의 접근성은 보다 미세한 개념이며 한 클래스가 다른 클래스의 필드나 메서드에 액세스할 수 있는지 여부를 고려합니다. 클래스 경계, 패키지 경계, 모듈 경계로 제공됩니다.
j1

이 두 규칙 사이의 관계는 가독성이 우선이고 접근성은 가독성을 기반으로 한다는 것입니다. 예를 들어 클래스가 public 이지만 내보내지 않은 경우 가독성 때문에 더 이상 사용할 수 없습니다. 그리고 비공용 클래스가 내보낸 패키지에 있는 경우 가독성은 통과를 허용하지만 접근성은 거부합니다.

가독성을 높이려면 모듈 선언에서 " requires " 지시문을 사용 하거나 명령줄에서 " -add-reads " 옵션을 지정하거나 Module.addReads 메서드를 호출할 수 있습니다. 같은 방식으로 경계 캡슐화를 해제하려면 모듈 선언에서 " opens " 지시문을 사용 하거나 명령줄에서 " –add-opens " 옵션을 지정하거나 Module.addOpens 메서드를 호출할 수 있습니다.

반사조차도 가독성 및 접근성 규칙을 깨뜨릴 수 없습니다. 그렇지 않으면 해당 오류 또는 경고가 발생합니다. 참고할 사항: 리플렉션을 사용할 때 런타임은 자동으로 두 모듈 사이에 가독성 에지를 설정합니다. 그것은 또한 무언가 잘못되면 접근성 때문이라는 것을 의미합니다.

2.2. 다양한 리플렉션 사용 사례

Java 모듈 시스템에는 명명된 모듈, 명명되지 않은 모듈, 플랫폼/시스템 모듈, 애플리케이션 모듈 등과 같은 다양한 모듈 유형이 있습니다.

j2

분명히 하기 위해 "모듈 시스템"과 "시스템 모듈"이라는 두 가지 개념이 혼동스럽게 들릴 수 있습니다. 따라서 "시스템 모듈" 대신 "플랫폼 모듈" 개념을 사용합시다.

위의 모듈 유형을 고려하면 서로 다른 모듈 유형 사이에 꽤 많은 조합이 존재합니다. 일반적으로 명명되지 않은 모듈은 자동 모듈을 제외하고는 명명된 모듈에서 읽을 수 없습니다. 불법 반사 액세스가 발생하는 세 가지 일반적인 시나리오만 살펴보겠습니다.

j3

위 그림에서 딥 리플렉션은 Reflection API를 사용하여 setAccessible(flag) 메서드를 호출하여 클래스의 비공개 멤버에 대한 액세스 권한을 얻는 것을 의미합니다 . 다른 명명된 모듈에서 명명된 모듈에 액세스하기 위해 리플렉션을 사용하면 IllegalAccessException 또는 InaccessibleObjectException 이 발생합니다 . 마찬가지로 리플렉션을 사용하여 명명되지 않은 모듈에서 module이라는 애플리케이션에 액세스할 때 동일한 오류가 발생합니다.

그러나 리플렉션을 사용하여 이름 없는 모듈에서 플랫폼 모듈에 액세스하면 IllegalAccessException 또는 경고가 표시됩니다. 경고 메시지는 문제가 발생한 위치를 찾고 추가 해결 방법을 찾는 데 유용합니다.

WARNING: Illegal reflective access by $PERPETRATOR to $VICTIM

위의 경고 메시지 형식에서 $PERPETRATOR는 반영 클래스 정보를 나타내고 $VICTIM은 반영 클래스 정보를 나타냅니다. 그리고 이 메시지는 완화된 강력한 캡슐화 에 기인합니다 .

2.3. 완화된 강력한 캡슐화

Java 9 이전에는 많은 타사 라이브러리가 리플렉션 API를 활용하여 마법 같은 작업을 수행했습니다. 그러나 모듈 시스템의 강력한 캡슐화 규칙은 대부분의 코드, 특히 JDK 내부 API에 액세스하기 위해 심층 반사를 사용하는 코드를 무효화합니다. 그것은 바람직하지 않습니다. Java 8에서 Java 9의 모듈식 시스템으로의 원활한 마이그레이션을 위해 완화된 강력한 캡슐화라는 타협이 이루어집니다.

완화된 강력한 캡슐화는 실행 프로그램 옵션 –illegal-access를 제공하여 런타임 동작을 제어합니다. –illegal-access 옵션은 이름 없는 모듈에서 플랫폼 모듈에 액세스하기 위해 리플렉션을 사용할 때만 작동한다는 점에 유의해야 합니다 . 그렇지 않으면 이 옵션이 적용되지 않습니다.

–illegal -access 옵션에는 네 가지 구체적인 값이 있습니다.

  • permit : 플랫폼 모듈의 각 패키지를 명명되지 않은 모듈로 열고 경고 메시지를 한 번만 표시합니다.
  • warn : " permit " 과 동일하나 , 불법 반사 액세스 작업마다 경고 메시지를 표시합니다.
  • debug : " warn "과 동일하며 해당 스택 추적도 인쇄합니다.
  • 거부 : 불법 반사 액세스 작업을 모두 비활성화합니다.

Java 9부터 –illegal-access=permit 가 기본 모드입니다. 다른 모드를 사용하려면 명령줄에서 이 옵션을 지정할 수 있습니다.

java --illegal-access=deny com.baeldung.module.unnamed.Main

Java 16에서는 –illegal-access=deny가 기본 모드가 됩니다. Java 17부터 –illegal-access 옵션이 완전히 제거되었습니다 .

3. 리플렉션 불법 액세스 수정 방법

Java 모듈 시스템에서 깊은 리플렉션을 위해서는 패키지가 열려 있어야 합니다.

3.1. 모듈 선언에서

우리가 코드 작성자라면 module-info.java 에서 패키지를 열 수 있습니다 .

module baeldung.reflected {
    opens com.baeldung.reflected.opened;
}

보다 신중하게 하기 위해 자격을 갖춘 열기를 사용할 수 있습니다 .

module baeldung.reflected {
    opens com.baeldung.reflected.internal to baeldung.intermedium;
}

기존 코드를 모듈식 시스템으로 마이그레이션할 때 편의를 위해 전체 모듈을 열 수 있습니다.

open module baeldung.reflected {
    // don't use opens directive
}

열린 모듈은 내부 opens 지시문을 허용하지 않는다는 점에 유의해야 합니다 .

3.2. 명령줄에서

우리가 코드 작성자가 아닌 경우 명령줄에서 –add-opens 옵션을 사용할 수 있습니다.

--add-opens java.base/java.lang=baeldung.reflecting.named

그리고 이름이 지정되지 않은 모든 모듈에 열기를 추가하려면 ALL-UNNAMED를 사용할 수 있습니다 .

java --add-opens java.base/java.lang=ALL-UNNAMED

3.3. 런타임 시

런타임에 열기를 추가하려면 Module.addOpens 메서드를 사용할 수 있습니다 .

srcModule.addOpens("com.baeldung.reflected.internal", targetModule);

위의 코드 스니펫에서 srcModule은 targetModule 에 대한 " com.baeldung.reflected.internal " 패키지를 엽니다 .

주목 해야 할 한 가지: Module.addOpens 메서드는 호출자에 민감합니다 . 이 메서드는 수정 중인 모듈, 공개 액세스 권한이 부여된 모듈 또는 이름이 지정되지 않은 모듈에서 호출할 때만 성공합니다. 그렇지 않으면 IllegalCallerException 이 발생합니다 .

대상 모듈에 열기를 추가하는 또 다른 방법은 Java 에이전트를 사용하는 것입니다 . java.instrument 모듈 에서 Instrumentation 클래스는 Java 9부터 새로운 redefineModule 메서드를 추가했습니다. 이 메서드는 추가 읽기, 내보내기, 열기, 사용 및 제공을 추가하는 데 사용할 수 있습니다.

void redefineModule(Instrumentation inst, Module src, Module target) {
    // prepare extra reads
    Set<Module> extraReads = Collections.singleton(target);

    // prepare extra exports
    Set<String> packages = src.getPackages();
    Map<String, Set<Module>> extraExports = new HashMap<>();
    for (String pkg : packages) {
        extraExports.put(pkg, extraReads);
    }

    // prepare extra opens
    Map<String, Set<Module>> extraOpens = new HashMap<>();
    for (String pkg : packages) {
        extraOpens.put(pkg, extraReads);
    }

    // prepare extra uses
    Set<Class<?>> extraUses = Collections.emptySet();

    // prepare extra provides
    Map<Class<?>, List<Class<?>>> extraProvides = Collections.emptyMap();

    // redefine module
    inst.redefineModule(src, extraReads, extraExports, extraOpens, extraUses, extraProvides);
}

위의 코드에서 먼저 대상 모듈을 활용하여 extraReads , extraExportsextraOpens 변수를 구성합니다 . 그런 다음 Instrumentation.redefineModule 메서드를 호출합니다 . 결과적으로 src 모듈은 대상 모듈 에 액세스할 수 있습니다 .

4. 결론

이 예제에서는 먼저 모듈 시스템의 가독성과 접근성을 소개했습니다. 그런 다음 다양한 불법 반사 액세스 사용 사례와 완화된 강력한 캡슐화가 Java 8에서 Java 9 모듈 시스템으로 마이그레이션하는 데 얼마나 도움이 되는지 살펴보았습니다. 마지막으로 불법 반사 접근을 해결하기 위한 다양한 접근 방식을 제공했습니다.

늘 그렇듯이 이 예제의 소스 코드는 GitHub 에서 찾을 수 있습니다 .

res – REST with Spring (eBook) (everywhere)