Reflection을 사용하면 프로그램이 실행 중에 클래스의 정보를 동적으로 가져올 수 있으며, 이를 통해 클래스의 멤버(필드, 메서드, 생성자)에 접근하고 수정할 수 있다.
Reflection은 런타임에 동적으로 클래스를 조작할 수 있는 유연성을 제공한다.
장점:
동적인 클래스 조작: 런타임에 클래스의 정보를 동적으로 알아내고 조작해 실행 중에 클래스의 멤버(메서드, 필드, 생성자)에 접근하고 수정할 수 있다.
유연한 프로그래밍: 런타임에 클래스의 구조를 분석하고 조작할 수 있으므로, 다양한 시나리오에서 동적으로 클래스를 조작하고 기능을 확장할 수 있다.
단점:
성능 저하: 동적으로 클래스를 조작하기 때문에 일반적인 메서드 호출보다 오버헤드가 발생할 수 있다.
런타임 오류: 컴파일 타임에 오류를 잡아내기 어려울 수 있고, 런타임에 예외가 발생할 가능성이 높아진다.
보안 위험: 접근 지시자를 우회하여 private 멤버에 접근할 수 있으므로 주의가 필요하다. 하단에 자세히 적었다.
Class<?> clazz = ClassName.class;
Class<?> clazz = instance.getClass(); // instance는 해당 클래스의 인스턴스
Field field = clazz.getDeclaredField("fieldName");
// private 필드에 접근 가능하도록 설정
field.setAccessible(true);
Object value = field.get(instance); // instance는 해당 클래스의 인스턴스
field.set(instance, newValue); // newValue는 필드에 설정할 값
Method method = clazz.getDeclaredMethod("methodName", parameterTypes);
// private 메서드에 접근 가능하도록 설정
method.setAccessible(true);
// insatnce는 해당 클래스의 인스턴스, args는 메서드의 매개변수 값 배열
Object result = method.invoke(insatnce, args);
Constructor<?> constructor = clazz.getDeclaredConstructor(parameterTypes);
// private 생성자에 접근 가능하도록 설정
constructor.setAccessible(true);
Object instance = constructor.newInstance(args); // args는 생성자의 매개변수 값 배열
객체 뿐만 아니라 Annotation도 조작이 가능하다.
Class<?> clazz = MyClass.class;
// 클래스에 적용된 모든 Annotation 가져오기
Annotation[] annotations = clazz.getAnnotations();
// 특정 Annotation 가져오기
AnnotationName annotation = clazz.getAnnotation(AnnotationName.class);
AnnotationName annotation = clazz.getAnnotation(AnnotationName.class);
String value = annotation.value();
int count = annotation.count();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(AnnotationName.class)) {
AnnotationName annotation = method.getAnnotation(AnnotationName.class);
String value = annotation.value();
int count = annotation.count();
// Annotation이 적용된 메서드 동적 호출
method.invoke(instance, args);
}
}
Reflection을 사용하면 가지는 가장 큰 단점인 보안 이슈이다.
접근 지시자 우회: setAccessible(true) 메서드를 호출하여 private 멤버에도 접근할 수 있다. 이는 클래스의 내부 상태를 무시하고 강제로 접근하므로, 불필요한 외부 접근과 데이터 누출의 위험성을 가진다.
보안 검사 우회: 컴파일러의 의도된검사를 우회하고 잠재적으로 안전하지 않은 동작을 수행할 수 있는 위험성을 가진다.
외부 코드 호출: 외부에서 악의적인 코드를 주입하여 실행할 수 있는 보안 위험을 초래할 수 있다. 잘못된 클래스 로딩 또는 실행으로 인해 시스템의 안전성과 안정성이 위협될 수 있다.
스프링에서도 Reflection을 상당히 많이 사용하고 있는데, IoC Container에서 Component Scan을 사용해 Bean을 등록하고, 의존성에 맞게 DI 할 때 사용된다.
AOP 에서도 사용되는데, 프록시 객체를 동적으로 생성하고 부가작업 할때도 사용된다.
캐싱과 메타데이터 활용
스프링은 Reflection을 통해 클래스와 메서드에 대한 정보를 한 번만 가져오고, 이를 캐싱하여 재사용하여 성능 저하를 최소화한다.
또한, 스프링은 애노테이션과 메타데이터를 활용하여 클래스, 메서드, 필드 등에 대한 정보를 사전에 정의하고 관리함으로써 Reflection을 최소한으로 사용하고 효율적으로 작동한다.
컴파일 타임 유효성 검사
스프링은 애노테이션 기반의 컴포넌트 스캔과 의존성 주입을 통해 컴파일 타임에 많은 유효성 검사를 수행한다. 컴파일 타임의 강력한 타입 체크와 유효성 검사는 런타임에 발생할 수 있는 에러를 사전에 방지하여 안정성을 높인다.
Reflection은 프로그램이 실행 중에 클래스의 정보를 동적으로 가져와서 조작할 수 있는 강력한 기능을 제공한다. 이를 통해 클래스의 멤버(필드, 메서드, 생성자) 및 Annotation 에 접근하고 조작할 수 있으며, 런타임 시점에 유연한 프로그래밍을 가능하게 한다.
동적으로 클래스 조작이 가능하지만, 성능 저하, 런타임 오류 발생 가능성, 보안 위험 등의 단점도 있다.
스프링도 Reflection을 사용하는데 IoC 컨테이너와 AOP 등 다양하게 사용한다.
스프링은 Reflection의 단점을 극복하기 위해 캐싱과 메타데이터 활용, 컴파일 타임 유효성 검사 등의 방법을 사용하여 성능을 최적화하고 안정성을 높인다.