reflection이란 구체적인 클래스 타입을 알지 못해도 클래스의 메소드, 타입, 변수들을 접근할 수 있게 해주는 자바의 API이다. JVM에서 실행되는 애플리케이션의 런타임 동작을 검사하거나 수정할 수 있는 기능이 필요한 프로그램에서 사용된다.
어플리케이션이 실행되고 스프링의 BeanFactory에서 객체가 호출될 때 객체의 인스턴스를 생성하는데, 이때 사용되는 기술이 바로 reflection이다. 이처럼 reflection은 프레임워크나 라이브러리에서 많이 사용된다. 사용자가 어떤 클래스를 만들지 예측 불가능하기 때문에 동적으로 해결하기 위한 reflection을 사용하는 것이다.
사용 예시를 코드와 함께 살펴보았다.
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 += 1;
}
public int getPosition() {
return position;
}
public static void main(String[] args) {
Object obj = new Car("foo", 0); - (1)
obj.move(); - (2)
}
Object 클래스는 모든 객체의 부모 객체이므로 (1)과 같이 사용이 가능하다. 자식 객체인 Car를 Object에 할당하고 있는 것이다. 하지만 (2)가 가능한가? 불가능하다.
자바는 컴파일러를 사용하는데, 컴파일 타임에 타입이 결정되기 때문이다. obj라는 이름의 객체는 Object 타입으로 결정이 나버려 Car의 메서드인 move를 찾을 수 없다. 따라서 위의 코드는 컴파일 에러가 난다.
이 경우 Reflection API를 사용할 수 있다.
public static void main(String[] args) {
Object obj = new Car("foo", 0);
Class car = Car.class; - (3)
Method move = car.getMethod("move");
move.invoke(obj, null); - (4)
Method getPosition = car.getMethod("getPosition");
int position = (int)getPosition.invoke(obj, null);
System.out.println(position);
(3)번과 같이 Reflection API를 사용해 클래스의 이름만으로 해당 클래스, 메서드, 생성자, 필드에 대한 정보를 가져올 수 있다.
(4)에서는 invoke 메서드를 통해 메서드를 실행시킬 객체와 해당 메서드에 넘길 인자를 지정하고 있다.
reflection은 다음과 같은 정보들을 가져올 수 있다. 이를 통해 객체의 생성, 메소드 호출, 변수 값 변경 등이 가능하다.
(1) 클래스 타입을 아는 경우
Class class = Car.class;
※ primitive 타입일 경우
Class class = boolean.class;
Class class = Double.TYPE;
Class class = Void.TYPE;
(2) 클래스의 이름만 알고 있는 경우
Class class = Car.forName("com.test.Car");
(1) 인자없는 생성자 리턴
Constructor constructor = class.getDeclaredConstructor();
(2) 인자가 String인 생성자 리턴
Constructor constructor = class
.getDeclaredConstructor(String.class);
(3) private, public 관계없이 모든 생성자 리턴
Constructor constructor = class
.getDeclaredConstructors();
(4) public 생성자만 리턴
Constructor constructor = class.getConstructors();
(1) 메소드 이름과 파라미터 정보와 일치하는 메소드 리턴
Method method = class
.getDeclaredMethod("method", int.class);
(2) 파라미터 정보 여러개인 경우
Class types[] = new Class[2];
types[0] = int.class;
types[1] = boolean.class;
Method method = class
.getDeclaredMethod("method", types);
(3) 모든 메소드 리턴, Super 클래스 정보 제외
Method method = class.getDeclaredMethods();
(4) 모든 메소드 리턴, Super 클래스 포함
Method method = class.getMethods();
(5) 메소드 호출, 파라미터 : 호출객체와 인자 값
method.invoke(class, 10);
(6) static 메소드 호출
method.invoke(null, 10);
(1) 이름과 일치하는 필드 리턴
Field field = class.getDeclaredField("field");
(2) 모든 필드 리턴, Super 클래스 정보 제외
Field field = class.getDeclaredFields();
(3) 모든 필드 리턴, Super 클래스 정보 포함
Field field = class.getFields();
(4) 인자값 변경, 파라미터 : 호출객체와 바꾸려는 값
field.set(class, "change value");
(5) static 필드 정보 가져오거나 변경하기
field.set(null, "change value);
field.get(null);
JVM이 실행되면 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어 static 영역에 저장된다. 따라서 reflection API는 이 정보를 활용해 static 영역을 뒤져서 정보를 가져오는 것이다.
구체적인 클래스 타입을 알지 못해도 클래스의 메소드, 필드 등에 접근할 수 있게 해 주지만 성능 오버헤드가 존재한다.
컴파일 타임이 아닌 런타임에 동적으로 타입을 분석하고 정보를 가져오기 때문에 JVM을 최적화할 수 없다.
또한 직접 접근할 수 없는 private 인스턴스 변수, 메서드에 접근이 가능(setAccessible)하여 추상화가 깨진다.
[참조] https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/
[참조] https://codechacha.com/ko/reflection/
[참조] https://medium.com/msolo021015/%EC%9E%90%EB%B0%94-reflection%EC%9D%B4%EB%9E%80-ee71caf7eec5