ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Reflection API란 무엇인가?
    Today I Learned 2023. 5. 27. 17:36

    private 메서드를 클래스 외부에서 억지로 호출할 수 있다고?

    이펙티브 자바에서 싱글턴을 다루는 부분을 살펴보다가 다음의 내용이 눈에 들어왔다.
     

    권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.

     
     
    private으로 선언한 메서드를 어떻게 호출할 수 있는 것인지 싶어 ChatGPT의 도움을 받아 통해 간단한 예시를 작성해보았다.
     
    예시에 사용할 객체를 살펴보자. 단 하나의 인스턴스만 존재함을 보장하기 위해 전역에서 해당 객체의 인스턴스에 직접 접근할 수 있도록 public static으로 인스턴스를 정의한 뒤, 맨 처음에 생성된 뒤로는 불변 상태를 유지하도록 final 예약어를 부여했다. 그리고 해당 객체는 생성자를 직접 호출할 수 없도록 생성자의 가시성을 private으로 지정했다.

    public class SingletonObject {
        public static final SingletonObject INSTANCE
            = new SingletonObject();
    
        private SingletonObject() {
            
        }
    
        public void doSomething() {
    
        }
    }

     
    다음의 테스트를 살펴보자. 두 개의 싱글턴 인스턴스가 서로 같은지 확인하는 테스트로, 싱글턴 객체라면 인스턴스가 하나만 존재해야 하므로 테스트는 성공해야 한다.

    class SingletonObjectTest {
        @Test
        void equals() throws InvocationTargetException,
            NoSuchMethodException,
            InstantiationException,
            IllegalAccessException {
            
            SingletonObject instance1
                = SingletonObject.INSTANCE;
            
            Class<?> classType = SingletonObject.class;
            Constructor<?> constructor = classType.getDeclaredConstructor();
            constructor.setAccessible(true);
            SingletonObject instance2 = (SingletonObject) constructor.newInstance();
            
            assertThat(instance1).isEqualTo(instance2);
        }
    }

     
    그러나 해당 테스트는 실패한다.
     

     
    이 말인 즉슨, 해당 객체에 대해 기존의 인스턴스에 더해 새로운 인스턴스가 추가로 생성되었다는 의미이다. 어떻게 생성자의 가시성이 private인데도 객체 외부에서 생성자를 호출해서 새로운 인스턴스를 생성할 수 있었을까?
     
     

    Reflection API

    리플렉션은 런타임에 특정 클래스, 메서드, 필드 등의 정보에 접근할 수 있도록 하는 Java의 API이다. 애플리케이션이 실행될 때 JVM의 메모리에는 바이트 코드로 변환된 소스코드의 클래스, 메서드, 필드 등의 정보들이 메타데이터를 포함하여 메모리의 static 영역에 적재되는데, 리플렉션은 런타임에 해당 메타데이터의 정보에 접근해 특정 클래스에 대한 정보를 획득한다.
     
    리플렉션의 활용은 클래스 객체를 획득하기 위해 다음의 방식들처럼 런타임에 특정 클래스의 정보에 접근하는 것으로부터 시작한다.

    // 객체.class
    Class<?> classType = SingletonObject.class;
    
    // 인스턴스.getClass()
    Class<?> classType = instance.getClass();
    
    // Class.forName("package 경로를 포함한 클래스명")
    Class<?> classType = Class.forName("java.util.ArrayList");

     
    본 글에서는 획득한 Class 객체를 이용해 런타임에 인스턴스를 생성하는 예시만 살펴보겠으나, 해당 Class 객체를 통해 정의된 메서드나 필드 변수의 이름, 부여된 값 등을 확인하고 필드의 값을 인위적으로 변경하는 것도 가능하다. 추가적인 활용 방법들을 확인하고자 하는 경우에는 다음의 링크를 참고할 수 있다.

     

    Using Java Reflection

    Reflection is a feature in the Java programming language. It allows an executing Java program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all

    www.oracle.com

     
    SingletonObject의 클래스 타입을 얻었다고 했을 때, 객체를 생성하기 위해서는 생성자에 접근할 수 있어야 한다. Class 클래스에 정의된 메서드인 getDeclaredConstructor() 메서드를 통해 생성자에 접근할 수 있다.
     

    Constructor<?> constructor = classType.getDeclaredConstructor();

     
    해당 예시에서는 생성자에 전달되어야 하는 인자들이 없기 때문에 getDeclaredConstructor()에 전달되는 인자들이 없지만, 특정 인자가 주어지는 생성자를 지정하기 위해서는 생성자에 맞는 인자의 Class 타입들을 해당 메서드에 인자들로 전달할 수 있다.
     
    이제 획득한 Constructor 객체를 이용해 새로운 인스턴스 생성을 시도해보자.
     

    SingletonObject instance2 = (SingletonObject) constructor.newInstance();

     
    해당 동작만을 수행하는 경우에는 가져온 Constructor 인스턴스의 생성자가 private 타입이기 때문에 다음과 같이 IllegalAccessException 예외가 발생한다.
     

     
    Constructor 객체는 자신에 대한 접근 가능 여부를 직접적으로 변경할 수 있는 .setAccessible() 메서드를 제공한다. 해당 메서드를 호출해 인스턴스 생성 시 접근 가능 여부를 가능하도록 상태를 변경해주면, 이제 해당 생성자의 접근 제어자가 private임에도 불구하고 새로운 인스턴스를 생성할 수 있다.
     

    constructor.setAccessible(true);
    
    SingletonObject instance2 = (SingletonObject) constructor.newInstance();

     

    치트키 같은 API이지만 수많은 단점들이 존재한다

    생성하려는 클래스에 대한 정보가 컴파일 시점에 전혀 존재하지 않아도 인스턴스가 생성되도록 할 수 있고, 해당 리플렉션 인스턴스에 한해 가시성까지 제어할 수 있다는 사실은 놀랍지만, 다음과 같은 수많은 단점들이 존재한다.

    좋지 못한 성능 및 예외 발생 가능성 증가

    런타임에 메타데이터에 직접 접근하는 특성 상, JVM의 최적화가 어렵기 때문에 리플렉션을 통한 동작 수행은 일반적인 코드를 실행하는 것에 비해 좋지 못한 성능을 보인다고 한다. 또한 런타임에 직접 전달되는 정보에 의존해야 하는 특성 상, 컴파일 시점에 확인할 수 없는 잘못된 정보가 전달될 경우 애플리케이션 실행 시 예외가 발생할 수 있고, 이를 처리하기 위해 리플렉션을 사용하지 않았을 때는 굳이 필요하지 않던 예외처리 로직들이 여럿 추가되어야 한다.

    소스코드의 복잡성 증가

    비즈니스 로직과 크게 상관없을 수 있는 Class, Constructor, Method 같은 로우 레벨에서 동작해야 할 메커니즘이 소스코드에 등장하다 보니, 소스코드의 가독성이 떨어지고 유지보수성이 저하될 수 있다.

    캡슐화의 저해

    앞서 살펴보았듯 리플렉션을 이용하면 사전에 지정되어 있던 접근 제어자를 무시하고 클래스의 특정 메서드나 필드에 직접 접근할 수 있다. 이로 인해 특정 데이터를 다루는 객체 자신만이 데이터에 접근할 수 있도록 하는 캡슐화와 같이 객체지향을 통해 얻을 수 있는 이점들을 저해할 수 있다.
     
     

    어떻게든 사용을 해보겠다면...

    사실 리플렉션은 일반적인 애플리케이션의 기능을 작성하는 과정에서는 구체적인 클래스의 타입을 컴파일 시점에 충분히 알 수 있는 경우가 많기 때문에 굳이 사용할 이유를 찾기 쉽지 않다. 기능 구현 상에서 어떻게든 사용할 만한 경우를 찾아보자면, 다음과 같이 이미 정의된 상위 클래스나 interface 타입의 객체의 인스턴스를 생성할 때, 하위 클래스나 구현체를 컴파일 시점에 결정해주는 정도로 사용해볼 수도 있겠으나, 사실 이조차도 굳이?라는 생각이 들기는 한다.

    public Set<String> create(String typeName)
        throws ClassNotFoundException,
        NoSuchMethodException,
        IllegalAccessException,
        InstantiationException,
        InvocationTargetException,
        ClassCastException {
        Class<? extends Set<String>> classType
            = (Class<? extends Set<String>>) Class.forName(typeName);
        Constructor<? extends Set<String>> constructor
            = classType.getDeclaredConstructor();
        return constructor.newInstance();
    }

     
    ChatGPT를 통해 확인한 그 외에 리플렉션을 활용할 수 있는 상황으로는 라이브러리나 프레임워크를 구현할 때 컴파일 시점에서는 알 수 없는 사용자가 정의한 클래스를 동적으로 파악하기 위해 사용하거나, 테스트 코드 작성 과정에서 결과를 정밀하게 검증하기 위해 사용하는 정도가 있었다.
     
     

    References

    - 이펙티브 자바 아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.
    - 이펙티브 자바 아이템 65. 리플렉션보다는 인터페이스를 사용하라. (테스트)
    - Package java.lang.reflect
    - Trail: The Reflection API
    - Using Java Reflection
    - Constructor.java
    - Reflection API 간단히 알아보자.
    - ChatGPT
     
     
     
     

    댓글

Designed by Tistory.