[Java] Reflection

Reflection이란

  • 클래스 로더가 클래스 정보를 메서드 영역이 생성하는데 런타임 환경에서 메서드 영역에 접근해서 해당 클래스의 정보(필드, 메서드, 클래스 등) 조작하는 방법
  • 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메서드, 타입, 변수 등등)에 접근할 수 있게 해주는 자바 API다.

 

사진: Unsplash 의 Thomas Grams

 

Reflection API

Class 객체에 접근해서 해당 클래스의 정보를 참조 및 수정, 실행을 할 수 있다. 

public class Coffee {

    private String name;

    private int price;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

public class Barista {

    private Coffee makeCoffee(Coffee coffee) {
        return coffee;
    }
}

 

Class 인스턴스를 불러오는 방법은 총 세 가지이다.

  • Barista 인스턴스에서 getClass() 메서드 호출
  • Barista 클래스에서 .class로 클래스 객체 접근
  • Class.forName("풀패키지 경로"); 를 통한 접근
@Test
public void test3() throws Exception {
    Class<?> baristaClass = Class.forName("com.reflection.example.Barista");
    Method makeCoffee = baristaClass.getDeclaredMethod("makeCoffee", Coffee.class);

    Class<Barista> baristaClass1 = Barista.class;

    Barista barista = new Barista();
    Class<? extends Barista> baristaClass2 = barista.getClass();


    Assertions.assertEquals(baristaClass.getDeclaredMethod("makeCoffee", Coffee.class).getName(), "makeCoffee");
    Assertions.assertEquals(baristaClass1.getDeclaredMethod("makeCoffee", Coffee.class).getName(), "makeCoffee");
    Assertions.assertEquals(baristaClass2.getDeclaredMethod("makeCoffee", Coffee.class).getName(), "makeCoffee");
}

 

Class 인스턴스로 해당 클래스의 인스턴스를 생성할 수 있다.

  • getConstructor(null) : 기본 생성자를 가져오는 함수
  • newInstance() : 생성자를 가져와서 새로운 인스턴스를 만드는 함수
  • (Barista)로 타입 캐스팅
@Test
public void test4() throws Exception {
    Class<?> baristaClass = Class.forName("com.reflection.example.Barista");
    Barista barista = (Barista) baristaClass.getConstructor(null).newInstance();
}

 

 

Reflection API를 통해 여러 정보를 접근할 수 있다. (해당 클래스의 필드, 메서드, 어노테이션, Enum, Interface 등)

접근하는 참조 메서드는 getXXX와 getDeclaredXXX로 나눈다.

  • getXXX
    • 참조 범위 : 자신과 상위 클래스 
    • public 한 접근 제어자만 가져올 수 있다.
  • getDeclaredXXX
    • 참조 범위 : 자신
    • 모든 접근 제어자를 가져올 수 있다.

 

아래의 코드는 getDeclaredFields()를 사용하여 private field에 접근하여 값을 변경하는 코드이다. 

@Test
public void test2() throws Exception {
    Coffee coffee = new Coffee();

    Class<? extends Coffee> coffeeClass = coffee.getClass();

    Field[] declaredFields = coffeeClass.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        declaredField.setAccessible(true); // private field도 접근할 수 있도록 true로 변경
        if (declaredField.getName().equals("name")) {
            declaredField.set(coffee, "latte");
        }

        if (declaredField.getName().equals("price")) {
            declaredField.set(coffee, 5000);
        }
    }

    Assertions.assertEquals(coffee.getPrice() , 5000);
    Assertions.assertEquals(coffee.getName(), "latte");
}

해당 클래스의 모든 필드를 get으로 가져와서 순회를 돌아서 name과 price 필드에 latte와 5000을 set하고 있다. 이렇듯 리플렉션을 사용하여 private한 필드에 접근하여 수정할 수 있다. 

아래의 코드는 private한 메서드에 접근하여 해당 메서드를 실행하는 코드이다.

@Test
public void test1() throws Exception {
    Coffee coffee = new Coffee();
    coffee.setName("americano");
    coffee.setPrice(4000);

    Barista barista = new Barista();

    Class<Barista> baristaClass = Barista.class;
    Method makeCoffee = baristaClass.getDeclaredMethod("makeCoffee", Coffee.class);
    makeCoffee.setAccessible(true);

    Coffee returnCoffee = (Coffee) makeCoffee.invoke(barista, coffee);
    Assertions.assertEquals(returnCoffee.getName(), coffee.getName());
    Assertions.assertEquals(returnCoffee.getPrice(), coffee.getPrice());
}

field와 마찬가지로 getDeclaredMethod("메서드명" , 파라미터 타입)을 호출하여 Method 인스턴스를 가져온 후  private도 접근할 수 있도록 setAccessible을 true로 변경 후 해당 method를 실행하고 있다.

 

Reflection 단점

Reflection API를 남발하게 되면 코드가 복잡해지고 , 성능이 저하된다. 오버헤드가 발생할 가능성이 있다. 또한 private 변수에 접근이 가능하면서 보안에도 취약해지고, 자바의 핵심인 다형성도 깨지게 된다. 이렇듯 Reflection의 단점은 여러 가지가 존재한다. 따라서 정말 필요한 상황이 아니면 가급적 reflection 사용은 지양해야 한다.

 

Coffee coffee = new Coffee();
coffee.setName("latte");
coffee.setPrice(5000):

Class<?> coffeeClass = Class.forName("com.example.Coffee");
Field nameField = coffeeClass.getDeclaredField("name");
nameField.set(coffee, "latte");
Field priceField = coffeeClass.getDeclaredField("price");
priceField.set(coffe, 5000);
  • Coffee 클래스의 인스턴스를 생성하고 setter로 private 필드를 설정해 주는 3줄의 코드도 reflection API를 사용하면 복잡해진다. (코드의 복잡성 증가, 성능 저하, 보안성 취약)

 

마무리 

Reflection 공부하면서 Spring DI가 어떻게 구현이 되는지, 개발자가 직접 짜는 코드 외에 프레임워크나 라이브러리에서 작동하는 코드들이 리플렉션 API를 사용하는 구나라는 점을 알게 되었다. 직접 Reflection API 사용하여 개발할 가능성은 아직 적지만, 이러한 기술이 있다는 것을 아는 것과 모르는 것에 차이는 크다고 생각이 든다. 사용하고 있는 프레임워크가 내부적으로 어떻게 돌아가는지 알고 있는 사람이 조금 더 그 기술을 적절하게 사용할 수 있기 때문이다. 

 

참고 자료

https://www.youtube.com/watch?v=RZB7_6sAtC4

https://www.inflearn.com/course/the-java-code-manipulation/dashboard

 

더 자바, 코드를 조작하는 다양한 방법 - 인프런 | 강의

여러분이 사용하고 있는 많은 자바 라이브러리와 프레임워크가 "어떻게" 이런 기능을 제공할 지 궁금한적 있으신가요? 이번 강좌를 통해 자바가 제공하는 다양한 코드 또는 객체를 조작하는 방

www.inflearn.com

https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/

 

Reflection API 간단히 알아보자.

Spring Framework를 학습하다 보면 Java Reflection API를 자주 접하게 된다. 하지만 Reflection API…

tecoble.techcourse.co.kr