Java

[Java] Reflection

devJK93 2024. 2. 21.

Java Reflection이란?


리플렉션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이며, 
컴파일 시간이 아닌 실행 시간(런타임)에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이다.

JVM 힙 영역에 로드된 Class(대문자 'C') 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API

여기서 로드된 클래스라고 함은, JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후, 해당 클래스의 정보를 담은 Class 타입의 객체를 생성하여 메모리의 힙 영역에 저장해 둔 것을 의미한다.
JVM 메모리 구조 (JAVA)

new 키워드를 통해 만드는 객체와는 다르다. (↓)

 

new 연산자 이용 객체 생성 vs 리플렉션 객체 생성

 


Reflection 언제 사용해야 할까?

  • 동적으로 클래스를 사용해야할 때 사용한다.
  • 다시 말해, 작성 시점에는 어떠한 클래스를 사용해야할지 모르지만 런타임 시점에서 가져와 실행해야하는 경우 필요하다.
  • 프레임워크나 IDE에서 이런 동적 바인딩을 이용한 기능을 제공한다.

리플렉션 사용 예시 

  • IntelliJ의 자동완성 기능

  • 스프링 어노테이션

     리플렉션을 사용하면 클래스와 메소드에 어떤 어노테이션이 붙어 있는지 확인할  있다. 어노테이션은  자체로는 아무 역할도 하지 않는다. 리플렉션 덕분에 우리가 스프링에서 @Component , @Bean 과 같은 어노테이션을 프레임워크의 기능을 사용하기 위해 사용할  있는 것이다. 

  • 스프링 프레임워크

    ➡ DI, Proxy, ModelMapper
    ➡ Hibernate, Lombok

😐 사용예시의 기술들이 어떻게 리플렉션을 활용하는지 정확히는 모르겠다.

런타임에 동적으로 클래스를 사용해야 할 경우에 Reflection을 사용해서 해결할 수 있다는 사실만 알 수 있다.


Class 클래스

리플렉션의 핵심은 Class 클래스라고 할 수 있다.

Class 클래스는 java.lang.Class 패키지에서 제공된다.

 

자바 리플렉션을 사용하기에 앞서, 힙 영역에 로드된 Class 타입의 객체를 가져와야 한다.

Class 타입의 객체를 얻는 방법 3가지

  • 클래스.class 로 가져오기
  • 인스턴스.getClass() 로 가져오기
  • Class.forName("클래스명") 으로 가져오기
public class Main {
  public static void main(String[] args) throws IOException, ClassNotFoundException {

    // 1. Object.getClass()
    // 모든 클래스의 최상위 클래스인 Object 클래스에서 제공하는 getClass() 메서드를 사용해 가져온다.
    Item item = new Item("itemA", 15000, 10);
    Class<? extends Item> clazz = item.getClass();


    // 2. 클래스 리터럴 (*.class)
    // 인스턴스가 존재하지 않고, 컴파일된 클래스 파일만 있다면 리터럴로 Class 객체를 얻을 수 있다.
    Class<? extends Item> clazz2 = Item.class;


    // 3. Class.forName()
    // 클래스의 도메인을 상세히 적어주어야 한다. 파일 경로에 오타가 있으면 에러가 발생한다.
    Class<?> clazz3 = Class.forName("com.example.hello.Practice$Item"); // {패키지명}.Item


    System.out.println(System.identityHashCode(clazz));
    System.out.println(System.identityHashCode(clazz2));
    System.out.println(System.identityHashCode(clazz3));
    
    // 결과
    303563356
    303563356
    303563356
  }
  
  static class Item {
    public String itemName;
    private int price;
    private int quantity;
    
    public static int staticNum = 100;

    public Item() {
    }

    public Item(String itemName, int price, int quantity) {
      this.itemName = itemName;
      this.price = price;
      this.quantity = quantity;
    }

    public void getInfo(String info) {
      System.out.println(info);
    }
    
    public static int getSum(int num1. int num2) {
      return num1 + num2;
    }

    private void getPrivateInfo() {
      System.out.println("private info");
    }

    @Override
    public String toString() {
      return "itemName: " + itemName + ", " + 
          "price: " + price + ", " + 
          "quantity: " + quantity;
    }
}
}

 

3가지 방법 모두 같은 Class 객체를 가리키는 것을 알 수 있다.

위의 방법으로 얻을 Class 객체는 클래스의 메타정보를 가지고 있다.

Class 클래스는 리플렉션을 위해서 사용되며 클래스의 구조, 메서드, 생성자 등과 관련된 정보를 제공한다.


 

Class 객체를 통해 할 수 있는 것들

 

동적으로 생성자를 가져와서 객체생성

  • getConstructor() 를 호출할 때, 인자로 생성자의 매개변수 타입을 바인딩해주어야 함
  • 매개변수 타입을 지정해주지 않으면 기본생성자가 호출
  • 해당하는 생성자가 없다면 NoSuchMethodException
    // Class.forName() 으로 Class 객체 가져오기
    Class<Item> clazz = (Class<Item>) Class.forName("com.example.hello.Practice$Item");

    // 생성자 가져오기
    Constructor<Item> constructor = clazz.getConstructor(String.class, int.class, int.class);

    // 가져온 생성자로 인스턴스 생성
    Item item = constructor.newInstance("itemB", 10000, 10);
    System.out.println(item);

여러개의 생성자를 가져올 수도 있다.

 

public 생성자만 가져올 수도 있다.

Class<Item> clazz = (Class<Item>) Class.forName("com.example.hello.Practice$Item");

Constructor constructors[] = clazz.getConstructors();
for (Constructor cons : constructors) {
    System.out.println("Get public constructors in Child: " + cons);
}

// 출력
Get constructors in Child: public com.example.hello.Practice$Item()

 

동적으로 메서드를 가져와서 실행하기

  • getMethod() 호출시에 인자로 생성자의 매개변수 타입을 바인딩 해줘야 함
  • 매개변수가 없는 메서드라면 메서드명만 입력
  • 메서드의 실행은 Method 에서 제공하는 invoke() 사용

    // Class.forName() 으로 Class 객체 가져오기
    Class<Item> clazz = (Class<Item>) Class.forName("com.example.hello.Practice$Item");

    // public 메서드
    // getMethod("메서드명", 매개변수 타입들)
    Method method1 = clazz.getMethod("getInfo", String.class);
    method1.invoke(new Item(), "getInfo");

    // static 메서드
    Method method2 = clazz.getMethod("getSum", int.class, int.class);
    int result = (int) method2.invoke(null, 10, 20);
    System.out.println("result = " + result);

    // private 메서드
    Method method3 = clazz.getDeclaredMethod("getPrivateInfo");
    // private -> 외부에서 access 할 수 있도록 설정 변경
    method3.setAccessible(true);
    method3.invoke(new Item());

 

 

getConstructors(), getDeclaredConstructors() 처럼 getMethod(), getDeclaredMethod() 를 활용해서 여러개의 메서드, public 메서드들만 추출할 수 있다.

Method methods[] = clazz.getMethods();
for (Method method : methods) {
    System.out.println("Get public methods in both Parent and Child: " + method);
}

Method methods2[] = clazz.getDeclaredMethods();
for (Method method : methods2) {
    System.out.println("Get public methods in both Parent and Child: " + method);
}

 

동적으로 필드 조작하기

  • getField() 를 통해서 클래스의 필드를 얻을 수 있음
  • 필드값 변경은 set() 메서드 호출
  • 필드는 클래스가 인스턴스가 되어야 Heap 메모리에 적재되기 때문에 인스턴스가 필요하다
  • static 필드는 Method Area에 있기 때문에 인스턴스 필요 없음
    // Class.forName() 으로 Class 객체 가져오기
    Class<Item> clazz = (Class<Item>) Class.forName("com.example.hello.Practice$Item");

    // static 필드를 가져와서 조작하고 출력
    Field staticNum = clazz.getField("staticNum");
    staticNum.set(null, 200);
    System.out.println(staticNum.get(null)); // 200

    Item item = new Item("itemC", 20000, 30);

    // public 필드 가져오기
    Field itemName = clazz.getField("itemName");

    // private 필드 가져오기
    Field price = clazz.getDeclaredField("price");
    // private -> 외부에서 access 할 수 있도록 설정
    price.setAccessible(true);

    // 필드 조작하기
    itemName.set(item, "itemModified");
    price.set(item, 35000);

    System.out.println(itemName.get(item)); // itemModified
    System.out.println(price.get(item)); // 35000

 

마찬가지로 여러개의 필드, public만 뽑아내는 메서드가 있다.

Field fields[] = clazz.getFields();
for (Field field : fields) {
    System.out.println("Get public fields in both Parent and Child: " + field);
}

Field fields2[] = clazz.getDeclaredFields();
for (Field field : fields2) {
    System.out.println("Get public fields in both Parent and Child: " + field);
}

리플렉션을 자제해야 하는 이유

일반적으로 메소드를 호출한다면, 컴파일 시점에 분석된 클래스를 사용하지만 리플렉션은 런타임에 클래스를 분석하므로 속도가 느리다. JVM을 최적화할 수 없기 때문이라고 한다. 그리고 이런 특징으로 인해 타입 체크가 컴파일 타임에 불가능하다. 또한 객체의 추상화가 깨진다는 단점도 존재한다.

 

따라서 일반적인 애플리케이션 개발자는 사실 리플렉션을 사용할일이 거의 없다. 보통 라이브러리나 프레임워크를 개발할 사용된다. 따라서 정말 필요한 곳에만 리플렉션을 한정적으로 사용해야한다.

 


[출처]

 

 

[Java] 리플렉션 (Reflection)이란 무엇일까? (개념/ 예시)

서론 이번 포스팅에서 다룰 내용은 '리플렉션'이다. 최근 "리플렉션이 무엇인가요?" 라는 질문을 받았는데, 제대로 된 답변을 못한 것 같다. C# 개발을 할 때 분명 사용은 해보았지만 개념적으로

jeongkyun-it.tistory.com

 

 

[Java] Reflection 개념 및 사용 방법

java-study에서 스터디를 진행하고 있습니다. Reflection이란? 리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메

steady-coding.tistory.com

 

 

[Java] 리플렉션(Reflection)

리플렉션(Reflection)이란? 리플렉션(Reflection)은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이다. 컴파일 시간이 아닌 실행시간

brightstarit.tistory.com

 

 

자바 리플렉션 (Reflection) 기초

리플렉션 (Reflection) JVM은 클래스 정보를 클래스 로더를 통해 읽어와서 해당 정보를 JVM 메모리에 저장한다. 그렇게 저장된 클래스에 대한 정보가 마치 거울에 투영된 모습과 닮아있어, 리플렉션

hudi.blog

 

 

 

[Java] 리플렉션 (reflection) 개념 이해하기

리플렉션 들어가며 자바를 처음 배우던 시절 생각해 보면 리플렉션이라는 단어조차 들어 본 적이 없었습니다. 자바를 점점 학습하면서 종종 들어봤지만 그때 당시 '리플렉션을 모르면 사용하지

ebabby.tistory.com

 

'Java' 카테고리의 다른 글

[Java] JDK, JRE, JVM 질의응답 (ChatGPT)  (1) 2024.08.31
Generics (Java)  (1) 2024.02.12
JVM 메모리 구조 (JAVA)  (1) 2024.02.08

댓글