본문 바로가기
Languages/Java

[Java] 리플렉션(Reflection)이란

by yoon_seon 2023. 5. 12.

스프링과 JPA를 학습하던 중 Reflection이라는 개념이 자주 등장하여 정리하기 위해 포스팅을 하게 되었다.

 

스프링은 어떻게 실행시점에 빈을 주입할 수 있는 걸까?

JPA의 Entity는 왜 꼭 기본 생성자를 가져야만 할까?

 

📌 Reflection

구체적인 클래스 타입을 알지 못해도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

 

자바에서는 컴파일러가 자바코드를 바이트코드로 바꿔주고 클래스로더는 바이트코드를 읽어 JVM 메모리 영역에 저장하게 된다.

리플렉션은 이 JVM 메모리 영역에 저장된 클래스의 정보를 꺼내와서 필요한 정보들 생성자, 필드, 메서드들을 가져와 사용하는 기술이다.

 

📌 Reflection은 왜, 어디에서 사용할까?

Why

개발자 입장에서 소스코드를 작성할 시점에는 객체의 타입을 알지만 프레임워크나 라이브러리 입장에서는 컴파일 시점에서 알 수 없다. 이러한 문제를 동적으로 해결하기 위해 사용한다.

 

Where

JPA, Jackson, Mockito, JUnit 등 많은 프레임워크나 라이브러리에서 리플렉션 기능을 이용하며  intelliJ의 자동완성 기능과 스프링의 어노테이션도 리플렉션을 이용한 기능이다. 동적으로 바인딩해서 기능을 제공하는 것이다.

 

📌 많은 프레임워크나 라이브러리에서 객체에 기본생성자가 필요한 이유

기본 생성자로 객체를 생성하고 필드를 통해 값을 넣어주는 것이 가장 간단한 방법이기 때문이다.

 

기본 생성자를 사용하지 않으면 다음과 같은 문제가 있다.

  1. 만약 기본생성자가 없다면 생성자의 종류가 많은 경우 어떤 생성자를 사용할 지 프레임워크나 라이브러리가 선택하기가 어렵다. 
  2. 생성자에 로직이 있는 경우 원하는 값이 변경될 수 있다.
  3. 생성자의 여러개의 파라미터 타입이 같은 경우 필드와 이름이 다르면 알맞은 값을 넣지 않을 가능성이 있다.

기본 생성자를 사용할 경우 이 모든 경우의 수를 고려하지 않아도 된다.

리플렉션으로 기본 생성자로 객체를 생성한뒤 필드 이름에 맞춰 알맞은 값을 넣어주면 되고 기본 생성자를 사용하는 것이 훨씬 간단하기 때문에 많은 프레임워크나 라이브러리가 기본생성자를 요구하는 것이다.

 

📌 Class 클래스

리플렉션의 기본은 Class이다.

Class 타입은 실행 중인 자바 애플리케이션의 클래스와 인터페이스의 정보를 가진 클래스로 클래스에 붙은 어노테이션, 생성자, 필드, 메서드, 부모클래스와 구현하고 있는 인터페이스를 조회할 수 있다.

Class 타입은 public 생성자가 존재하지 않고 JVM에 자동으로 객체가 생성된다.

생성된 Class 객체를 3가지 방법으로 가지고 올 수 있다.

1. {클래스타입}.class

Class<?> clazz = Pizza.class;

2. {인스턴스}.getClass()

Pizza pizza = new Pizza();
Class<?> clazz = pizza.getClass();

3. Class.forName("{전체 도메인 네임}")

Class<?> clazz = Class.forName("Pizza") // 패키지 경로까지 써야한다.

 

클래스 타입의 메서드를 사용할 때 주의해야 할 점

  • getMethods() : 상위 클래스와 상위 인터페이스에서 상속한 메서드를 포함하여 public인 메서드들을 모두 가져온다.
  • getDeclaredMethods() : 접근 제어자와 관계없이 상속한 메서드를 제외하고 직접 클래스에서 선언한 메서드들을 모두 가져온다.

getXXX와 getDeclaredXXX를 잘 구분해서 사용해야 한다.

 

📌 Reflection 예제

예제를 위해 Pizza 클래스를 생성

public class Pizza {
    private final String secretCode = "ABCDE";
    private Integer price;

    private Pizza() {
        this.price = 10000;
    }
    public Pizza(int price) {
        this.price = price;
    }
    public int getPrice() {
        return this.price;
    }
    private void secretRecipe(String secretCode) {
        if (this.secretCode.equals(secretCode)) {
            System.out.println("비밀 레시피");
        }
    }
    public void recipe() {
        System.out.println("오픈된 레시피");
    }
}

 

생성자 찾기 및 객체 생성

public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("Pizza");
    // 생성자 조회
    Constructor<?> constructor1 = clazz.getDeclaredConstructor();
    Constructor<?> constructor2 = clazz.getDeclaredConstructor(int.class);
    // 객체 생성
    Object o1 = constructor1.newInstance();
    Object o2 = constructor2.newInstance(10000);
}

getDeclaredConstructor() 메서드를 이용해 생성자의 파라미터로 구분하여 클래스에 선언된 생성자를 가져올 수 있다.

newInstance() 메서드를 가지고온 생성자를 통해 객체를 생성할 수 있다.

 

만약 getDeclaredConstructor() 메서드로 가지고 온 생성자의 접근제어자가 private 라면 해당 오류가 발생한다.

접근제어자가 public이 아닌 경우 setAccessible() 메서드를 이용하면 접근 할 수 있다.

public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("Pizza");
    // 생성자 조회
    Constructor<?> constructor1 = clazz.getDeclaredConstructor();
    Constructor<?> constructor2 = clazz.getDeclaredConstructor(int.class);
    // private 생성자에도 접근 가능
    constructor1.setAccessible(true);
    // 객체 생성
    Object o1 = constructor1.newInstance();
    Object o2 = constructor2.newInstance(10000);
}

 

필드 찾기 및 변경

public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("Pizza");
    Constructor<?> constructor1 = clazz.getDeclaredConstructor();
    constructor1.setAccessible(true);

    // 필드 조회
    Object obj = constructor1.newInstance();
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        field.setAccessible(true); // private 필드에 접근
        System.out.println(field);
        System.out.println("field : "+field.get(obj));
    }
    
    // 필드 변경
    Field field = clazz.getDeclaredField("price");
    field.setAccessible(true); // private 필드에 접근
    System.out.println("기존 : "+field.get(obj));
    field.set(obj, 20000);
    System.out.println("변경 : "+field.get(obj));
}

 

getDeclaredFields() 메서드를 이용해 필드의 접근제어자, 타입, 필드명, value 등의 정보를 얻을 수 있다.

getDeclaredField("{필드명}") 메서드를 이용해 필드를 가져와 set() 메서드로 필드를 수정할 수 있다.

 

메서드 찾기 및 호출

public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("Pizza");
    Constructor<?> constructor1 = clazz.getDeclaredConstructor();
    constructor1.setAccessible(true); // private 생성자 접근
    Object obj = constructor1.newInstance();

    // 메서드 찾기
    Method[] methods = clazz.getDeclaredMethods();
    for (Method method : methods) {
        method.setAccessible(true); // private 메서드 접근
        System.out.println(method);
    }
    // 메서드 실행
    Method method = clazz.getDeclaredMethod("secretRecipe",String.class);
    method.setAccessible(true); // private 메서드 접근
    method.invoke(obj,"ABCDE");
}

getDeclaredMethods 메서드를 이용해 메서드의 접근제어자, 리턴 타입, 메서드명, 파라미터 타입 등의 정보를 가져올 수 있다.

getDeclaredMethod("{메서드명}",{파라미터 타입}) 메서드를 사용해 메서드의 정보를 가져온 후

invoke("{리플래션 인스턴스}",{파라미터})를 사용하여 메서드를 실행할 수 있다.

 

📌 어노테이션 

어노테이션이 작동되는 원리 또한 리플렉션 덕분이다.

  1. 리플렉션을 통해 클래스나, 메서드, 파라미터 정보를 가져온다.
  2. 리플렉션의 getAnnotation(s), getDeclaredAnnotation(s) 등의 메서드를 통해 원하는 어노테이션이 붙어 있는지 확인한다.
  3. 어노테이션이 붙어 있다면 원하는 로직을 수행한다.

 

📌 Reflection 단점

  • 리플렉션  API는 컴파일 시점이 아니라 런타임 시점에 클래스를 분석하므로 JVM을 최적화할 수 없기 때문에 성능저하가 발생한다.
  • 리플렉션은 런타임 시점에 클래스 정보를 알게되기 때문에 컴파일 시점에서 타입 체크 기능을 사용할 수 없다. 그렇기 때문에 Exception이 터질 가능성이 매우 많다.
  • 코드가 지저분하고 장활해진다.
  • 내부를 노출해서 추상화를 파괴한다. 리플렉션을 사용하면 접근할 수 없는 필드나 메서드에도 접근할 수 있고 모든 클래스의 정보를 알게된다. 추상화를 파괴하고 따라서 불변성 또한 지킬 수 없다.

따라서 리플렉션은 클래스의 정보를 컴파일 시점에 알아야 하는 특수한 경우가 아니라면 리플렉션 사용을 지양하는 것이 좋다.꼭 필요한 경우 아주 제한된 형태로만 사용하도록 하자.

 


참고 : https://www.youtube.com/watch?v=67YdHbPZJn4

댓글