반응형

1. 개요

 다이나믹 프록시를 공부하던 중 리플렉션이라는 개념이 두둥등장하였다. 간단하게 개념만 짚고 넘어가려했으나, Spring DI의 동작원리와 밀접하고, 프레임워크를 이해하는데 중요한 개념이라고 판단되어 자세히 알아보았다.

 


2. Reflection이 뭔가요?

 

2.1. 사전적 의미

 많은 영상이나 글에서 리플렉션에 대해 설명할 때 사전적 의미를 짚고 넘어간다. 사전적 의미와 유사한 기능을 하는 개념들이 많은데, 리플렉션 또한 이와 같기도 하고, 개념을 이해하기가 쉬운 편은 아니라 그런 것 같다. 사전적 의미는 다음과 같다.

 

1. (거울에 비친) 상, 모습
2. 반사

 

이제 이 사전적 의미를 기술적 의미와 함께 이해해보자.

 

2.2. 기술적 의미

런타임 단계에서 클래스의 정보를 분석해내는 자바 API로 클래스의 메서드, 타입, 필드, 어노테이션 등의 정보를 접근하거나 수정할 수 있다.

 

 정리하면 리플렉션이란 클래스의 정보 통해 '거울에 비친 상'과 같이 똑같은 형태를 만들고 이를 통해 메서드, 타입, 필드, 어노테이션과 같은 자원에 접근하거나 수정할 수 있는 자바 API이다.

 그렇다면, 실제 클래스와 똑같은 형태를 가진 정보는 대체 어디서 얻어오는 걸까??

 

리플렉션을 보고있는 클래스

 

 

2.3. 어디서? JVM에서!

 정확히는 JVM의 메모리 영역에서 가져온다.

 어플리케이션을 실행하면 작성한 자바 코드컴파일러에 의해 .class 형태의 바이트 코드로 변환되고, 이 정보들은 클래스 로더를 통해 JVM 메모리 영역에 저장된다. 그리고 클래스 정보를 통해 객체가 생성된다면 이는 JVM 힙 영역에 저장된다. 즉, JVM의 메모리영역에서 클래스의 정보를 가져올 수 있다.

런타임 시 JVM 의 동작과정 (출처 : 우아한 테크 파랑, 아키의 리플렉션)

 

2.4. 리플렉션이란?!

 다시! 리플렉션이란, 어플리케이션이 실행되어 JVM 메모리 영역에 클래스 정보들이 저장된 시점인 '런타임' 시에 이 영역에 접근하여 클래스의 정보를 분석, 수정하는 작업을 하는 API바로 자바 리플렉션이다!

 


3. 리플렉션 실습

 리플렉션 API를 테스트하는 간단한 실습을 해보자.

 

3.1. 초간단 Human 클래스 생성

public class Human {

    private String name;

    public Human(String name){
        this.name = name;
    }

    private Human(){

    }

    public void goRestRoom(){
        System.out.println(name +"이 화장실로 갑니다.");
    }

    public void offPants(){
        System.out.println(name +"이 바지를 내립니다.");
    }

    public void doWork(){
        System.out.println(name + "이 볼일을 봅니다.");
        poopOut();
    }

    private void poopOut(){
        System.out.println("똥이 나왔습니다.");
    }
}

 화장실에서 볼일을 보는 Human 클래스를 생성하였다. name 파라미터를 받는 생성자 메서드private 접근 제어자를 가진 기본 생성자 메서드를 생성하였다. poopOut 메서드는 외부에서 호출되는 것을 막기 위해 private 접근 제어자로 설정하였다.

 

3.2. 클래스 정보 조회하기

먼저 JVM에 저장될 클래스 정보를 조회하는 코드이다. 아래와 같이 크게 세가지 방법이 있다.

// JVM에 있는 클래스 정보 가져오기
Class<?> class1 = human.getClass();
Class<?> class2 = Human.class;
Class<?> class3 = Class.forName("org.example.reflection.Human");

 

3.3. 생성자 조회 및 호출

 이제 리플렉션 기능을 사용해보자. 먼저 클래스의 생성자 정보를 가져오고 이를 호출해보도록 하겠다.

 

3.3.1. getConstructor()

// JVM에 있는 클래스 정보 가져오기
Class<?> class1 = Class.forName("org.example.reflection.Human");

// 리플렉션을 통해 생성자 가져오기
Constructor<?> constructor1 = class1.getConstructor(); // NoSuchMethodException !!
Constructor<?> constructor2 = class1.getConstructor(String.class);

// 가져온 생성자를 통해 객체 생성하기
Object human1 = constructor1.newInstance();
Object human2 = constructor2.newInstance("승갱이");

 위 코드를 실행시키면 메서드 호출 시 NoSuchMethodException이 발생한다. 리플렉션 기능을 통해 생성자 메서드 정보를 가져오려 시도하였으나, 기본 생성자의 접근 제어자가 private 라 메서드를 찾지 못해 발생했다. 접근 제어자에 관계 없이 클래스 정보를 가져오려면 getConstructor() 대신 getDeclaredConstructor() 메서드를 사용하면 된다.

 

getXXX와 getDeclaredXXX의 차이 이해하기

더보기

getXXX vs getDeclaredXXX


리플렉션에서 호출하는 대부분의 메서드는 getXXX, getDeclaredXXX 처럼 쌍을 이루고 있다. 아래의 특징을 숙지하여 상황에 맞게 사용해야 한다.

getXXX
 상위 클래스와 상위 인터페이스에서 상속한 메서드를 포함하여 public인 값들을 가져온다. private와 같은 메서드를 조회할 경우 NoSuchMethodException 예외가 발생한다.

getDeclaredXXX
 접근 제어자와 관계 없이 상속한 메서드들을 제외하고 직접 클래스에서 선언한 값들을 가져온다.

 

3.3.2. getDeclaredConstructor()

// JVM에 있는 클래스 정보 가져오기
Class<?> class1 = Class.forName("org.example.reflection.Human");

// 리플렉션을 통해 생성자 가져오기
Constructor<?> constructor1 = class1.getDeclaredConstructor();
Constructor<?> constructor2 = class1.getDeclaredConstructor(String.class);

// 가져온 생성자를 통해 객체 생성하기
Object human1 = constructor1.newInstance(); // IllegalAccessException !!
Object human2 = constructor2.newInstance("승갱이");

 

 이로써 private로 선언된 생성자 정보는 가져왔으나, 생성자를 통해 객체 생성 시 IllegalAccessException이 발생했다. 이유는 접근 제어자가 private 이기 때문에 외부 호출이 불가능하기 때문이다. 앞서 발생한 예외는 클래스의 정보에서 기본 생성자 메서드를 찾지 못해 발생했고, 이번 예외는 해당 메서드를 호출하지 못해 발생한 것이다.

이를 해결하기 위해서 Human 클래스의 기본 생성자를 public으로 수정하여야 할까? 아니다. 리플렉션을 통해 private 메서드에도 접근할 수 있도록 조작하면 된다. 

 

3.3.3. setAccessible(true)

 setAccessible(true) 메서드를 통해 해당 생성자에 접근할 수 있도록 설정하였다. 여기서 중요한 점은 클래스를 수정하지 않고, 리플렉션을 통해 클래스의 생성자 정보를 조작한 후 호출까지 했다는 점이다.

// JVM에 있는 클래스 정보 가져오기
Class<?> class1 = Class.forName("org.example.reflection.Human");

// 리플렉션을 통해 생성자 가져오기
Constructor<?> constructor1 = class1.getDeclaredConstructor();
Constructor<?> constructor2 = class1.getDeclaredConstructor(String.class);

constructor1.setAccessible(true); // 해당 생성자에 접근할 수 있도록 설정

// 가져온 생성자를 통해 객체 생성하기
Object human1 = constructor1.newInstance();
Object human2 = constructor2.newInstance("승갱이");

 

기본 생성자를 통해 생성된 Human 객체

 

3.4. 멤버필드 조회하기

 다음은 리플렉션 기능을 사용하여 클레스의 멤버필드를 조회해보자.

 

3.4.1. getFields()

Class<?> class1 = Class.forName("org.example.reflection.Human");

for(Field field : class1.getFields()){
    System.out.println(field);
}

 Human 클래스에 name 멤버필드가 있지만 콘솔에 조회되지 않았다. 클래스 정보를 조회했더니 name은 찾을 수 없어 조회가 되지 않았다. 이유는 name의 접근제어자가 private이기 때문이다. getDeclaredFields() 메서드를 사용해야 한다.

 

3.4.2. getDeclaredFields()

Class<?> class1 = Class.forName("org.example.reflection.Human");

for(Field field : class1.getDeclaredFields()){
    System.out.println(field); // private java.lang.String org.example.reflection.Human.name
}

 private 접근제어자를 가진 필드 정보도 조회됨을 알 수 있다.

 

 3.4.3. 객체에 대한 멤버필드 조회하기

 이제 위 리플렉션 기능을 사용하여 객체를 생성하고, 해당 객체의 필드 정보를 조회해보자.

 호출한 생성자의 접근제어자는 public이므로 setAccessible 메서드를 사용하지 않았다. 하지만 객체의 name 필드는 접근 제어자가 private이므로 field.get(human) 메서드를 호출하기전 리플렉션에 대한 접근 설정을 true로 설정하였다.

Class<?> class1 = Class.forName("org.example.reflection.Human");

Constructor constructor = class1.getDeclaredConstructor(String.class);
Object human = constructor.newInstance("승갱이");

for(Field field : class1.getDeclaredFields()){
    System.out.println(field);
    field.setAccessible(true);
    System.out.println("value : "+ field.get(human));
}

출력결과

 

3.4.4. 객체에 대한 멤버필드 수정하기

 단순히 조회 뿐 아니라 멤버필드의 값도 수정할 수 있다. Setter 메서드가 없어도, 접근제어자가 private라도 이를 무시하고 값을 바꿔버릴 수 있는 아주 강력한 녀석임을 알 수 있다.

Class<?> class1 = Class.forName("org.example.reflection.Human");

Constructor constructor = class1.getDeclaredConstructor(String.class);
Object human = constructor.newInstance("승갱이");

for(Field field : class1.getDeclaredFields()){
    System.out.println(field);
    field.setAccessible(true);
    field.set(human, "변경된 승갱이"); // human 객체의 field 값 변경
    System.out.println("value : "+ field.get(human));
}

출력결과

 

3.5. 메서드 조회 및 호출하기

 이번엔 메서드의 정보를 조회하고 호출해보자.

Class<?> class1 = Class.forName("org.example.reflection.Human");

Constructor<?> constructor = class1.getConstructor(String.class);
Object human = constructor.newInstance("승갱이");

Method goRestRoomMethod = class1.getDeclaredMethod("goRestRoom");
Method offPantsMethod = class1.getDeclaredMethod("offPants");
Method doWorkMethod = class1.getDeclaredMethod("doWork");
Method poopOutMethod = class1.getDeclaredMethod("poopOut");

poopOutMethod.setAccessible(true);
poopOutMethod.invoke(human);
goRestRoomMethod.invoke(human);
offPantsMethod.invoke(human);
doWorkMethod.invoke(human);

 

 poopOut 메서드만 접근제어자가 private이므로 invoke 메서드 호출 전에 setAccessible(true) 메서드를 호출해주었다. 필자의 의도는 poopOut 메서드의 접근 제어자를 private 로 생성하여 바지를 내리기 전에 똥을 싸거나, 화장실에 들어가기 전에 똥을 싸는 불상사를 막으려 했는데, 리플렉션을 사용하니 똥을 먼저 싸버리는 걸 볼 수 있다.

다시한번 리플렉션의 강력함(?)을 느낄 수 있는 부분이다.

출력결과

 


4. 어디서 사용하나요?

 근데 이런 기능들을 대체 어디서 사용할까? 필자가 이 글을 쓰는 이유인 '다이나익 프록시' 라는 API에서도 사용하나, 대부분의 프레임워크나 라이브러리에서도 리플렉션 기능을 사용한다. 프레임워크나 라이브러리에서는 들어오는 클래스의 정보를 모르기 때문이다.

 코드를 작성한 개발자는 당연히 내가 작성한 클래스의 정보를 알 수 있지만, 프레임워크 입장에서 보면 모르는게 당연하다. 이때 리플렉션을 통해 런타임 시 클래스의 정보를 얻고 이를 기반으로 하여 프레임워크나 라이브러리가 지원하는 기능을 수행하는 것이다. 스프링의 주요 기능인 DI도 리플렉션의 원리가 들어있다.

 


5. 리플렉션을 통한 DI 프레임워크 구현해보기

 DI를 지원하는 초간단 프레임워크를 구현해보았다.

 

5.1. SSKAutowired

먼저 커스텀 어노테이션을 구현하였다. 특정 클래스의 멤버필드에 @SSKAutowired 어노테이션이 붙어있을 경우 해당 리플렉션을 통해 객체를 생성하기 위함이다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SSKAutowired {
}

 

5.2. Robot

 기본 생성자를 갖는 간단한 Robot 를 구현하였다. 특정 클래스의 멤버필드로 사용되며, DI를 위해 @SSKAutowired를 붙여줄 예정이다.

public class Robot {

    public void fight(){
        System.out.println("로봇이 싸웁니다.");
    }

    public void clean(){
        System.out.println("로봇이 청소합니다.");
    }

    private void destroy(){
        System.out.println("로봇이 파괴됩니다.");
    }
}

 

5.3. TestService

 @SSKAutowired가 붙은 robot 멤버필드를 갖고, robot의 기능을 추상화한 메서드를 갖는 클래스이다. 테스트 단계에서 robot 객체가 주입됐는지 확인하기 위해 getRobot 메서드도 추가하였다.

public class TestService {

    @SSKAutowired
    private Robot robot;

    public Robot getRobot(){
        return robot;
    }
    public void start(){
        robot.fight();
        robot.clean();
    }
}

 

5.4. CustomApplicationContext

 특정 클래스를 스캔하여 필요한 의존성을 주입해주는 클래스이다. getInstance(TestService.class) 메서드를 호출할 경우 @SSKAutowired 멤버필드에 대한 의존성이 주입된 TestService 객체를 생성 및 리턴한다.

public class CustomApplicationContext {

    /**
     * 클래스의 멤버필드 중 SSKAutowired가 붙어있을 경우 의존성 주입
     * @param clazz - 스캔 클래스
     * @return - 의존주입이 완료된 스캔 클래스
     * @throws Exception
     */
    public static <T> T getInstance(Class<T> clazz) throws Exception{

        T instance = createInstance(clazz);
        Arrays.stream(clazz.getDeclaredFields()).forEach(field -> {
            if(field.getAnnotation(SSKAutowired.class) != null){ // SSKAutowired가 붙은 멤버필드일 경우
                try {
                    Object fieldInstance = createInstance(field.getType()); // 멤버필드에 대한 객체 생성
                    field.setAccessible(true);
                    field.set(instance, fieldInstance); // 생성된 객체를 instance에 셋팅 (DI)
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
        return instance;
    }

    /**
     * 리플렉션 기본 생성자를 통해 객체 생성
     * @param clazz - 클래스 타입
     * @return 클래스 객체
     * @throws Exception
     */
    private static <T> T createInstance(Class<T> clazz) throws Exception{
        Constructor<T> constructor = clazz.getDeclaredConstructor(); // 리플렉션을 통해 클래스의 기본생성자 정보 조회
        constructor.setAccessible(true);
        return constructor.newInstance(); // 객체 생성
    }
}

 

5.5. Test

 CustomApplicationContext의 테스트 코드이다. TestService를 파라미터로 한 getInstance 메서드를 호출하면 의존성이 주입된 TestService 객체를 리턴받고, 확인하는 메서드이다. testService.start()를 통해 콘솔에 출력도 해보았다.

@Test
void getInstance() throws Exception {

    TestService testService = CustomApplicationContext.getInstance(TestService.class);

    assertNotNull(testService.getRobot());
    testService.start();
}

출력결과

 


6. 강력한 단점

 강력한 리플렉션, 그만큼 단점도 강력하기 때문에 사용 시 굉장한 주의를 요구한다.

 

6.1. 일반 메서드 호출보다 성능이 떨어진다.

 리플렉션은 동적으로 클래스를 생성하기 때문에 JVM 컴파일러가 최적화 할 수 없다. 컴파일 시에는 타입이 정해지지 않았기 때문이다. 해당 클래스의 타입이 맞는지, 생성자가 존재하는지 등의 벨리데이션 과정을 런타임 시 처리해야하기 때문에 성능이 떨어진다.

 

6.2. 컴파일 시 타입 체크가 불가능하다.

 리플렉션은 런타임 시점에 클래스의 정보를 알게 되므로 컴파일 시점에 타입 체크가 불가능하다.

 

6.3. 추상화를 파괴한다.

 리플렉션을 사용하는 모든 클래스의 정보를 알 수 있다. 외부로 노출시키지 않기 위해 private 접근제어자를 사용해도 접근할 수 있다. 즉, 추상화가 파괴된다.

 


7. 참고

파랑, 아키의 리플렉션 - https://www.youtube.com/watch?v=67YdHbPZJn4 

자바 리플렉션 - https://roadj.tistory.com/7

리플렉션의 단점 - https://middleearth.tistory.com/72

반응형

+ Recent posts