Java

Reflection API (리플렉션)

DEV_GOLF 2022. 12. 6. 21:48
반응형

Reflection API란?

reflection은 힙 영역에 로드된 class 타입의 객체를 통해 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고 인스턴스의 필드와 메소드를 접근 제어자와 상관없이 사용할 수 있도록 해준다.

실제로 reflection을 이용하여 Spring이나 Java 직렬화(jackson) 또는 JPA 같은 기술에서 많이 사용되고있는 기술이다.

제공해주는 method를 통해서 알아보자

public class Test {

    public static void main(String[] args) {
        Class<? extends String> fooClass = "foo".getClass();
        System.out.println(fooClass);
        System.out.println(boolean.class);

        // 결과 : boolean
        // 결과 : class java.lang.String
    }
}

getClass() 메소드는 클래스 정보를 로드하여 출력한다. 하지만 원시형 타입인 boolean이나 int 같은 타입은 .class를 이용하여야한다.

public class Test {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> collection = Class.forName("java.util.Collections");
        System.out.println(collection);

        Class<?> double = Class.forName("[D");
        double[][] doubleArr = new double[3][4];

        System.out.println(double == double.getClass());

        Class<?> string = Class.forName("[[Ljava.lang.String;");
        String[][] stArr = new String[3][4];

        System.out.println(string == stArr.getClass());

        // 출력 결과 
        // class java.util.Collections
        // true
        // true
    }
}

Class.forName을 이용하면 FQCN 정보를 받아올 수 있다. 또한 class와 interface를 구분하여 값을 반환한다. 또한 [[L로 2차배열 [ 로 1차배열의 정보를 반환받을 수 있다.

그 외에도 여러

  • class.getSuperClass() : 슈퍼 class 정보 반환
  • class.getDeclaredClass() : 명시적으로 선언된 class 정보 반환
  • class.getDeclaringClass() : class에 구성된 클래스(명시적으로 선언된)를 반환
  • class.getEnclosingClass() : class의 즉시 동봉된 클래스를 반환
public class Test {

    public static void main(String[] args) throws Exception {
        Class<? extends Member> memberClass = Member.class;
        Arrays.stream(memberClass.getConstructors()).forEach(System.out::println);

        Constructor<? extends Member> memberConstructor = memberClass.getConstructor();
        Member member = memberConstructor.newInstance();
        System.out.println(member);

        Constructor<? extends Member> constructor = memberClass.getConstructor(String.class, int.class);
        Member fullMember = constructor.newInstance("golf", 27);
        System.out.println(fullMember);

        Field[] fields = memberClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            System.out.println(field.get(fullMember));
        }
        fields[0].set(fullMember, "GOLF");
        System.out.println(fullMember);

        Method secretMessage = memberClass.getDeclaredMethod("secretMessage");
        secretMessage.setAccessible(true);
        secretMessage.invoke(fullMember);

        // 출력
        // public me.golf.blog.Member()
        // public me.golf.blog.Member(java.lang.String,int)
        // Member{name='null', age=0}
        // Member{name='golf', age=27}
        // golf
        // 27
        // Member{name='GOLF', age=27}
        // 쉿 이건 비밀이야 ..!
    }
}
public class Member {

    private final String name;
    private final int age;

    private Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Member() {
    }

    private void secretMessage() {
        System.out.println("쉿 이건 비밀이야..!");
    }

    @Override
    public String toString() {
        return "Member { " + name + ", " + age + " }";
    }
}

위 코드는 Member 클래스 정보를 가져오고 있다. 하지만 보다 싶이 private으로 필드와 메서드 생성자가 막혀있지만 외부에서 문제없이 실행하고 있다. 비밀은 reflection에 있다.

  1. getConstructor() 메소드는 class에 생성자를 가져올 수 있다. 아무리 private으로 막아 놨다고 하더라도 reflection api를 이용하면 접근이 가능해진다.
  2. memberClass.getConstructor(String.class, int.class) 처럼 타입을 명시해주면 타입에 맞는 생성자를 반환한다. 마찬가지로 private이더라도 접근이 가능해진다.
  3. private field 또한 memberClass.getDeclaredFields()를 이용하고 setAccessable(true) 옵션만 넣어주면 접근이 가능하다.
  4. getDeclaredMethod("secretMessage"), secretMessage.setAccessible(true), secretMessage.invoke(fullMember)를 이용해서 private 메서드에 접근하여 실행시킬 수 있다.

위 방법을 이용하여 실제로 직렬화를 할 때 Default 생성자가 private이더라도 접근하여 프록시 객체를 만들 수 있다.

Dynamic proxy

public interface Movable {

    void goAhead();
    void goBack();
    void stop();
}
public class MovableImpl implements Movable {

    @Override
    public void goAhead() {
        System.out.println("앞으로 간다.");
    }

    @Override
    public void goBack() {
        System.out.println("뒤로 간다.");
    }

    @Override
    public void stop() {
        System.out.println("멈춘다.");
    }
}
public class MovableProxy implements InvocationHandler {

    Movable movable;

    public MovableProxy(Movable movable) {
        this.movable = movable;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        switch (method.getName()) {
            case "goAhead":
                System.out.println("go-ahead proxy");
                return method.invoke(movable, args);
            case "goBack":
                System.out.println("go-back proxy");
                return method.invoke(movable, args);
            case "stop":
                System.out.println("stop proxy");
                return method.invoke(movable, args);
            default:
                throw new IllegalArgumentException("메서드 이름이 잘못됐습니다.");
        }
    }
}

예제 코드를 보자 interface를 정의하고 interface를 구현한 클래스를 만들어주고 InvocationHandler 를 이용하여 Dynamic proxy를 만들 수 있도록 해주었다. 이렇게 하면 기존 직접 Proxy를 만들면서 강제로 똑같은 동작을 함에도 구현해주어야 한다는 것과 모든 메서드를 일일히 구현해주어야 하는 번거로움을 많이 줄일 수 있다.

자 그럼 테스트를 해보자

public class Test {

    public static void main(String[] args) {
        Movable movable = (Movable) Proxy.newProxyInstance(
                Test.class.getClassLoader(),
                new Class[]{Movable.class},
                new MovableProxy(new MovableImpl()));

        movable.goAhead();
        movable.goBack();
        movable.stop();

        // go-ahead proxy
        // 앞으로 간다.
        // go-back proxy
        // 뒤로 간다.
        // stop proxy
        // 멈춘다.
    }
}

정상적으로 프록시가 동작하고 있다.

하지만 프록시가 어떻게 생성되는지 잘 모르겠다. 깊게 들어가보자 newProxyInstance 를 통하여 proxy객채를 만들고 있다.

reflection에 접근하여 프록시 생성자에 접근하고 접근한 생성자를 이용하여 새로운 프록시 인스턴스를 생성해준다. (내부적으로는 인터페이스 정보를 갖고와 클래스 바이트 파일을 만들어준다고 한다.) 

 

핵심 로직은 다음과 같다.  class 파일을 읽어와 새로운 class파일을 읽어오고 있다. 

 

마무리 요약을 하자면 reflection은 구체적인 객체 정보가 추상화 되어 알지 못하더라도 접근해줄 수 있도록 자바에서 제공해주는 API이고 API를 이용하여 AOP, JPA 등 많은 기술에서 사용되고 있다. 

이 때 Dynamic proxy가 많이 이용되는데 이는 reflection API에서 바이트 코드를 직접 조작하여 class 파일을 만들어 동적으로 프록시 객체를 만들어준다. 

 

마침. 

 

https://taes-k.github.io/2021/05/15/dynamic-proxy-reflection/

 

몰라도 되는 Spring - 리플렉션으로 만드는 Dynamic proxy

Dynamic proxy 이전 AOP Proxy 포스팅에서 Spring 에서 사용하는 Dynamic proxy와 CGLib를 이용한 proxy에 대해 말씀드리면서 다음과 같은 설명이 있었습니다. JDK dynamic proxy Reflection을 통해 동적으로 proxy 객체

taes-k.github.io