[Java] 리플렉션(Reflection)이란
스프링과 JPA를 학습하던 중 Reflection이라는 개념이 자주 등장하여 정리하기 위해 포스팅을 하게 되었다.
스프링은 어떻게 실행시점에 빈을 주입할 수 있는 걸까?
JPA의 Entity는 왜 꼭 기본 생성자를 가져야만 할까?
📌 Reflection
구체적인 클래스 타입을 알지 못해도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
자바에서는 컴파일러가 자바코드를 바이트코드로 바꿔주고 클래스로더는 바이트코드를 읽어 JVM 메모리 영역에 저장하게 된다.
리플렉션은 이 JVM 메모리 영역에 저장된 클래스의 정보를 꺼내와서 필요한 정보들 생성자, 필드, 메서드들을 가져와 사용하는 기술이다.
📌 Reflection은 왜, 어디에서 사용할까?
Why
개발자 입장에서 소스코드를 작성할 시점에는 객체의 타입을 알지만 프레임워크나 라이브러리 입장에서는 컴파일 시점에서 알 수 없다. 이러한 문제를 동적으로 해결하기 위해 사용한다.
Where
JPA, Jackson, Mockito, JUnit 등 많은 프레임워크나 라이브러리에서 리플렉션 기능을 이용하며 intelliJ의 자동완성 기능과 스프링의 어노테이션도 리플렉션을 이용한 기능이다. 동적으로 바인딩해서 기능을 제공하는 것이다.
📌 많은 프레임워크나 라이브러리에서 객체에 기본생성자가 필요한 이유
기본 생성자로 객체를 생성하고 필드를 통해 값을 넣어주는 것이 가장 간단한 방법이기 때문이다.
기본 생성자를 사용하지 않으면 다음과 같은 문제가 있다.
- 만약 기본생성자가 없다면 생성자의 종류가 많은 경우 어떤 생성자를 사용할 지 프레임워크나 라이브러리가 선택하기가 어렵다.
- 생성자에 로직이 있는 경우 원하는 값이 변경될 수 있다.
- 생성자의 여러개의 파라미터 타입이 같은 경우 필드와 이름이 다르면 알맞은 값을 넣지 않을 가능성이 있다.
기본 생성자를 사용할 경우 이 모든 경우의 수를 고려하지 않아도 된다.
리플렉션으로 기본 생성자로 객체를 생성한뒤 필드 이름에 맞춰 알맞은 값을 넣어주면 되고 기본 생성자를 사용하는 것이 훨씬 간단하기 때문에 많은 프레임워크나 라이브러리가 기본생성자를 요구하는 것이다.
📌 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("{리플래션 인스턴스}",{파라미터})를 사용하여 메서드를 실행할 수 있다.
📌 어노테이션
어노테이션이 작동되는 원리 또한 리플렉션 덕분이다.
- 리플렉션을 통해 클래스나, 메서드, 파라미터 정보를 가져온다.
- 리플렉션의 getAnnotation(s), getDeclaredAnnotation(s) 등의 메서드를 통해 원하는 어노테이션이 붙어 있는지 확인한다.
- 어노테이션이 붙어 있다면 원하는 로직을 수행한다.
📌 Reflection 단점
- 리플렉션 API는 컴파일 시점이 아니라 런타임 시점에 클래스를 분석하므로 JVM을 최적화할 수 없기 때문에 성능저하가 발생한다.
- 리플렉션은 런타임 시점에 클래스 정보를 알게되기 때문에 컴파일 시점에서 타입 체크 기능을 사용할 수 없다. 그렇기 때문에 Exception이 터질 가능성이 매우 많다.
- 코드가 지저분하고 장활해진다.
- 내부를 노출해서 추상화를 파괴한다. 리플렉션을 사용하면 접근할 수 없는 필드나 메서드에도 접근할 수 있고 모든 클래스의 정보를 알게된다. 추상화를 파괴하고 따라서 불변성 또한 지킬 수 없다.
따라서 리플렉션은 클래스의 정보를 컴파일 시점에 알아야 하는 특수한 경우가 아니라면 리플렉션 사용을 지양하는 것이 좋다.꼭 필요한 경우 아주 제한된 형태로만 사용하도록 하자.
참고 : https://www.youtube.com/watch?v=67YdHbPZJn4