Reflection API (리플렉션)
Reflection API란?
- 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메서드, 타입, 변수 등등)에 접근할 수 있게 해주는 자바 API다.
사용하는 Library, Framework, API, Feature
- Jackson, GSON 등의 JSON Serialization Library
- Log4 j2, Logback 등의 Logging Framework
- Apache Commons BeanUtils 등의 Class Verification API
- Spring의 @Autorwired와 같은 DL, DI 기능 (: processInject(), inject() Method )
- Spring Contatiner의 BeanFactory에서 사용
- 내부적으로 Spring의 ReflectionUtils라는 Abstraction Library를 사용한다.
- Eclipse, Intellij 등의 IDE, Junit, Hamcrest와 같은 Test Framework
우아한테크코스 미션을 진행하면서 사실상 적용이 안되었던 곳이 없었다..! 공부하면서도 자주 봤었는데 매번 그런 게 있다...하고 넘어갔는데 이번 기회에 정리하려고 한다.
리플렉션 API
- Class.newInstance() : 주어진 클래스의 인스턴스를 생성
- Class.getName() : 클래스의 이름을 반환
- Class.getMethods() : 클래스에 선언된 모든 public 메서드의 목록을 배열로 반환
- Method.invoke() : 해당 메서드를 호출
- Method.getParameterTypes() : 메서드의 매개변수 목록을 배열로 반환
public class Car {
private final String name;
private int position;
public Car(String name, int position) {
this.name = name;
this.position = position;
}
public void move() {
this.position++;
}
public int getPosition() {
return position;
}
}
자바의 특징 중 하나인 다형성 덕분에 아래와 같이 객체 생성이 가능하다.
public static void main(String[] args) {
Object obj = new Car("foo", 0);
}
하지만 car의 메서드인 move()를 사용하는 obj.move()는 불가능하다.
왜냐하면 자바는 기본적으로 컴파일러를 사용하기 때문이다. (물론 자바는 컴파일러와 인터프리터를 모두 사용하는 JIT 컴파일러 이다.)즉, 컴파일 시점에 타입을 결정한다. 따라서 컴파일 시점에 Object 타입인 obj는 런타임에 Car객체를 받아도 move()를 실행할 수 없게 된다.
public static void main(String[] args) throws Exception {
Object obj = new Car("foo", 0);
Class carClass = Car.class;
Method move = carClass.getMethod("move");
// move 메서드 실행, invoke(메서드를 실행시킬 객체, 해당 메서드에 넘길 인자)
move.invoke(obj, null);
Method getPosition = carClass.getMethod("getPosition");
int position = (int)getPosition.invoke(obj, null);
System.out.println(position);
// 출력 결과: 1
}
위와 같이 Reflection API는 클래스의 이름만 가지고도 이미 로딩이 완료된 클래스에서 또 다른 클래스를 동적으로 로딩(Dynamic Loading)하여 생성자, 멤버 필드 그리고 멤버 메서드 등을 사용할 수 있도록 한다.
또한 Reflection이 가져올 수 없는 정보 중 하나가 바로 생성자의 인자 정보들이다. 따라서 기본 생성자 없이 파라미터가 있는 생성자만 존재한다면 Reflection이 객체를 생성할 수 없게 되는 것이다.
어떻게 가능할까?
자바에서는 자바 컴파일러가 사용자가 작성한 자바 코드를 바이트 코드로 변환하여 heap 영역에 저장한다. Reflection API는 이 정보를 활용한다. 그래서 클래스 이름만 알고 있다면 언제든 heap 영역을 뒤져서 정보를 가져올 수 있는 것이다.
This information is store off heap in the PermGen (< Java 7) or MetaSpace (Java 8+) You can't see it from Java directly.
static영역에 저장하는 것인가 라는 말도 있지만, stackoverflow에서 위와 같이 MetaSpace에 heap영역에 저장된다고 한다.
(Java Compiler 를 통해서 .class 확장자를 가진 클래스 파일은 각 디렉터리에 흩어져 있고 JVM의 ClassLoader가 각각의 클래스 파일들을 찾아서 JVM 의 메모리에 탑재해준다.)
* ServiceLoader는 라이브러리에 있는 특정 인터페이스의 구현 클래스를 동적으로 찾아낼 수 있습니다.
장점
- Runtime 시점에서 사용할 Instance를 선택하고 동작시킬 수 있는 유연성을 제공한다.
단점
- Compile time에 Type, Exception 등의 검증을 진행할 수 없다. Runtime에서 가져오기 때문이다.
- Runtime에서 Instance가 선택되기 때문에 해당 로직의 구체적인 동작 흐름을 파악하는 것에 대해 어려움을 가지게 된다.
- Private 접근 제어자로 캡슐화된 필드, 메서드에 대해 접근 가능하기 때문에(캡슐화도 깨짐) 기존 동작을 무시하고 깨트리는 행위가 가능해진다. Singleton 객체, Internal API 사용 등
이런 단점이 많지만 Spring을 사용할 때는 Reflection API가 필수적이다. 왜냐하면 Spring Container의 BeanFactory를 사용해야 할 때 필요하기 때문이다.
Bean은 애플리케이션이 실행한 후 런타임에 객체가 호출될 때 동적으로 객체의 인스턴스를 생성하는데 이때 Spring Container의 BeanFactory에서 리플렉션을 사용한다.
Reflection API는 느리다.
리플렉션을 수행할 때 모든 단계에서 유효성을 검사해야하기 때문에 느리다. 예를들어 메서드를 호출할 때, 대상이 실제로 메서드 선언자의 인스턴스인지, 올바른 수의 인수가 있는지, 인수 유형은 올바른지 등 컴파일 시점에 확인이 불가능하기 때문에 매번 확인해야 하므로 JIT로 캐싱되지도 않고 매번 인터프리터로 읽어야하므로 성능상 느리다.
---
JPA에서 Entity에 기본 생성자를 protected를 해줘야 하는 이유
Spring Data JPA에서 Entity에 기본 생성자가 필요한 이유도 JPA는 DB 값을 객체 필드에 주입할 때 기본 생성자로 객체를 생성한 후 이러한 Reflection을 사용하여 값을 매핑하기 때문이다. 이때 지연 로딩으로 되어있으면 프록시 객체가 생성되어 실제 메서드를 호출할 때 엔티티를 불러와서 호출을 한다.
그런데 이 proxy 객체는 직접 만든 Entity를 상속하기 때문에 public 혹은 protected 기본 생성자가 필요하게 된다. 결국 public, protected 생성자가 없다면 이러한 proxy 객체를 사용할 수 없을 것이다.
Reflection API로 가져올 수 없는 정보 중 하나가 생성자의 인자 정보이다. 그래서 기본 생성자가 반드시 있어야 객체를 생성할 수 있는 것이다. 기본 생성자로 객체를 생성만 하면 필드 값 등은 Reflection API로 넣어줄 수 있다.
Reference
https://lob-dev.tistory.com/entry/Java%EC%9D%98-Reflection-API
https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/
https://stackoverflow.com/questions/36833232/how-jvm-stores-meta-information-of-a-class