Java의 리플렉션(Reflection)
개요
- 개념
- 어떻게 특정 Class 인스턴스를 획득할 수 있을까?
- 장점
- 단점
- 실무에서 사용할뻔했던 사례
- 참고 자료
1. 개념
Java
의 리플렉션(Reflection)
은 런타임에 클래스, 메서드, 필드 등의 정보를 확인 및 변경 가능하도록 한다.
쉽게 말해, 프로그램 실행 중에 객체의 구조나 메서드를 동적으로 사용할 수 있게 해주는 기술이다.
보통 Java
에서 객체를 사용할 때, 우리는 컴파일 시에 클래스와 메서드를 명시적으로 정의하고 호출한다.
public class Hello {
public void sayHello() {
System.out.println("Hello, World!");
}
public static void main(String[] args) {
// Reflection 없이 메서드 호출
Hello hello = new Hello();
hello.sayHello(); // 직접적으로 메서드를 호출
}
}
그러나 리플렉션
을 사용하면 컴파일 시에 어떤 클래스가 사용될 지 몰라도, 런타임에 해당 클래스를 찾아내고 그 클래스의 메서드나 필드를 동적으로 호출할 수 있다.
import java.lang.reflect.Method;
public class Hello {
public void sayHello() {
System.out.println("Hello, World!");
}
public static void main(String[] args) {
try {
// 클래스 정보 가져오기
Hello hello = new Hello();
Class<?> clazz = hello.getClass();
// 잘못된 메서드 이름을 사용해보자 ("sayHello" 대신 "sayHi" 사용)
Method method = clazz.getMethod("sayHi"); // 예외 발생 가능
// 메서드 호출
method.invoke(hello);
} catch (Exception e) { // 모든 예외를 한 번에 처리
System.out.println("예외 발생: " + e.getMessage());
}
}
}
물론 해당하는 필드를 찾지 못했을때에 대한 예외 처리는 필수이다.
2. 어떻게 특정 Class 인스턴스를 획득할 수 있을까?
Class<Member> aClass = Member.class; // (1)
Member member1 = new Member();
Class<? extends Member> bClass = member1.getClass(); // (2)
Class<?> cClass = Class.forName("hudi.reflection.Member"); // (3)
(출처 - [Hudi님의 리플렉션] https://hudi.blog/java-reflection)
첫 번째 방법으로는 클래스의 class
프로퍼티를 통해 획득하는 방법이다.
두 번째 방법으로는 인스턴스의 getClass()
메서드를 사용하는 것이다.
세 번째 방법으로는 class
의 forName()
정적 메서드에 FQCN(Fully Qualified Class name) 을 전달하여 해당 경로에 대응하는 클래스
에 대한 Class
클래스의 인스턴스를 얻는 방법이다.
이런 Class
의 객체는 Class
에 public
생성자가 존재하지 않아 우리가 직접 생성할 수 있는 방법은 없다. 대신 Class
의 객체는 JVM이 자동으로 생성 해준다.
2-1. getXXX() vs getDelcaredXXX()
Class
객체의 메소드 중 getFields()
, getMethods()
, getAnnotations()
와 같은 형태와getDeclaredFields()
, getDeclaredMethods()
, getDeclaredAnnotations()
와 같은 형태로 메소드가 정의되어 있는 것을 확인할 수 있다. 이 메소드들은 클래스에 정의된 필드, 메소드, 어노테이션 목록을 가져오기 위해 사용된다.
편의상 이 두 형태를 각각 getXXX()
, getDeclaredXXX()
로 부르겠다. 이 둘은 무엇이 다른 것일까? getXXX()
는 상속받은 클래스와 인터페이스를 포함하여 모든 public 요소를 가져온다. 예를 들면 getMethods()
는 해당 클래스가 상속받은 그리고 구현한 인터페이스에 대한 모든 public 메소드를 가져온다.
반면, getDeclaredXXX()
는 상속받은 클래스와 인터페이스를 제외하고 해당 클래스에 직접 정의된 내용만 가져온다. 또한 접근 제어자와 상관없이 요소에 접근할 수 있다. 예를 들어 getDeclaredMethods()
는 해당 클래스에만 직접 정의된 private, protected, public 메소드를 전부 가져온다.
3. 장점
핵심으로 유연성을 챙길 수 있다.
컴파일 시에 타입을 알 수 없더라도, 런타임 시에 객체를 조작할 수 있어서 동적인 프로그램을 만들 수 있다.
더군다나 리플렉션은 Spring의 필수 개념이다.Spring
에서 기본적으로 사용되는 @Component
@Bean
과 같은 어노테이션도 결국은 리플렉션을 기반으로 동작하기 때문이다.
뿐만아니라 @Getter
@Setter
과 같은 기본적인 어노테이션도 물론 리플렉션을 기반으로 사용된다.
4. 단점
성능 저하가 큰 단점이다. 리플렉션을 사용하면 일반적인 메서드보다는 호출 속도가 느리다.
그 다음으로 안정성 문제도 있는데, 리플렉션을 사용하려면 예외처리는 필수로 고려해야 한다.
마지막으로 OOP의 캡슐화 위반이 있다.
리플렉션은 private
메서드나 필드에도 접근할 수 있는데, 이 말은 결국 캡슐화를 꺠트린다는 말이 된다.
때문에 리플렉션은 치트키처럼 강력한 기능이지만, 성능과 안정성 면에서 주의가 필요되므로 정말 필요한 곳이 아니면 가급적 쓰지 않는것이 좋다.
5. 실무에서 사용할뻔했던 사례
private <T> Page<T> combineParticipantAndProductName(Page<T> responses, Map<Long, String> productNames) {
responses.getContent().forEach(participant -> {
try {
participant.getClass().getMethod("setPrizeName", Map.class).invoke(record, productNames);
} catch (Exception exception) {
log.error("Error while setting prizeName", exception.getMessage());
}
});
return responses;
}
유저의 정보에 상품 이름을 맵핑하는 로직이 여기저기 반복해서 쓰이는 상황이었다.
문제는 매개변수로 쓰인 유저의 객체 클래스는 저마다 달랐고, 마찬가지로 반환 객체도 저마다 달랐다.
때문에 Generic
으로 인풋과 아웃풋을 설정한 다음, 리플렉션
으로 상품 이름을 셋팅했다.
그러나 위에서 언급했던대로 리플렉션은 성능과 보안적인 관점에서 이슈가 있다.
이와 관련해서 코드 리뷰의 피드백이 들어왔고, 과연 리플렉션이 최선일까라는 생각이 들었다.
생각해보니 해당 로직을 작성했던 이유는, 인풋과 아웃풋 객체 모두 prizeName
을 반드시 갖는다는 전제하에 작성한 로직이므로, 굳이 리플렉션을 적용하지 않고 객체들을 공통 부모
로 묶어줘서 셋팅하면 되겠다 싶었다.
private <T extends ParticipantCommonResponse> Page<T> combineParticipantAndProductName(Page<T> responses, Map<Long, String> productNames) {
responses.getContent().forEach(participant -> participant.setPrizeName(productNames));
return responses;
}
리플렉션은 강력한 기능이지만, 성능 + 보안 + OOP에 패널티가 있다는 점을 고려하면,
정말 필요한 순간인지 잘 판단해서 쓰는게 좋을 것 같다.