Java Reflection이란?
리플렉션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이며,
컴파일 시간이 아닌 실행 시간(런타임)에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이다.
JVM 힙 영역에 로드된 Class(대문자 'C') 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API
여기서 로드된 클래스라고 함은, JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후, 해당 클래스의 정보를 담은 Class 타입의 객체를 생성하여 메모리의 힙 영역에 저장해 둔 것을 의미한다.
JVM 메모리 구조 (JAVA)
new 키워드를 통해 만드는 객체와는 다르다. (↓)
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' 카테고리의 다른 글
[Java] JDK, JRE, JVM 질의응답 (ChatGPT) (1) | 2024.08.31 |
---|---|
Generics (Java) (1) | 2024.02.12 |
JVM 메모리 구조 (JAVA) (1) | 2024.02.08 |
댓글