- Java Reflection
- 리플렉션이란?
- 리플렉션 사용 예시
- 리플렉션은 언제 사용되는가?
AOP와 프록시를 정리한 내용에서 리플렉션이 언급되었는데, 리플렉션이 무엇인지 정리해 보았다.
리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API이다.
여기서 로드된 클래스란, JVM 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후, 해당 클래스의 정보를 담은 Class 타입의 객체를 생성해 메모리의 힙 영역에 저장해 둔 것을 의미한다.
구체적인 Class Type을 알지 못하더라도 해당 Class의 method, type, variable들에 접근할 수 있도록 해주는 자바 API이며, 컴파일된 바이트 코드를 통해 런타임에 동적으로 특정 클래스의 정보를 추출할 수 있다.
리플렉션으로 할 수 있는 것들을 나열해 보면 다음과 같다.
리플렉션을 사용하는 경우 private으로 선언된 필드나 메서드에도 접근이 가능하다. 하지만 private 데이터에 접근하는 것 자체가 캡슐화를 깨트리는 위험성이 있으므로 주의해야 한다.
또한 리플렉션은 메서드 호출이나 필드 접근 시, JVM은 메타데이터를 통해 객체의 구조를 탐색하고 해당 요소를 찾아야 한다. 런타임에 바인딩이 이루어지게 되므로 추가 리소스를 소모하게 되고, 컴파일 시 에러 검출이 불가능해진다.
리플렉션 시 JVM의 메타데이터에 접근하게 되는데, 이를 탐색하는 작업이 일반적인 메서드 호출보다 더 많은 비용이 발생하며 필드나 메서드가 많을수록 비용이 더 발생할 수 있다.
리플렉션은 캐싱을 사용하지 않으므로, 동일한 필드나 메서드에 대해 반복적으로 리플렉션을 사용하더라도 매번 메타데이터를 재탐색해 이에 대한 비용이 발생한다.
다음 코드는 GPT를 통해 위에서 나열한 리플렉션으로 할 수 있는 것들을 수행하는 코드이다.
import java.lang.annotation.*;
import java.lang.reflect.*;
// 예제 클래스 정의
@CustomAnnotation
class ExampleClass extends ParentClass implements ExampleInterface {
private String privateField;
public int publicField;
public ExampleClass() {}
public ExampleClass(String privateField) {
this.privateField = privateField;
}
public void exampleMethod() {
System.out.println("Example method executed");
}
private void privateMethod() {
System.out.println("Private method executed");
}
}
class ParentClass {}
interface ExampleInterface {}
@Retention(RetentionPolicy.RUNTIME)
@interface CustomAnnotation {}
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 대상 클래스
Class<?> clazz = ExampleClass.class;
// 1. 필드 가져오기
System.out.println("Fields:");
for (Field field : clazz.getDeclaredFields()) {
System.out.println(" - " + field.getName());
}
// 2. 메서드 가져오기
System.out.println("\nMethods:");
for (Method method : clazz.getDeclaredMethods()) {
System.out.println(" - " + method.getName());
}
// 3. 상위 클래스 가져오기
System.out.println("\nSuperclass:");
System.out.println(" - " + clazz.getSuperclass().getName());
// 4. 인터페이스 가져오기
System.out.println("\nInterfaces:");
for (Class<?> iface : clazz.getInterfaces()) {
System.out.println(" - " + iface.getName());
}
// 5. 어노테이션 가져오기
System.out.println("\nAnnotations:");
for (Annotation annotation : clazz.getAnnotations()) {
System.out.println(" - " + annotation.annotationType().getName());
}
// 6. 생성자 가져오기
System.out.println("\nConstructors:");
for (Constructor<?> constructor : clazz.getConstructors()) {
System.out.println(" - " + constructor);
}
// 7. 생성자를 통해 객체 생성하기
System.out.println("\nCreating object using constructor:");
Constructor<?> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance("Reflection Example");
System.out.println("Instance created: " + instance);
// Private 메서드 접근
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true); // private 접근 허용
privateMethod.invoke(instance); // 메서드 호출
}
}
스프링 프레임워크는 리플렉션을 광범위하게 사용하여 런타임에 다양한 기능을 제공한다. 주요 활용 사례는 다음과 같다.
프록시에 대한 내용을 정리할 때 어떻게 리플렉션을 활용하고 프록시 객체를 사용하는지는 정리했었다. 굉장히 광범위하게 사용되고 있는 것을 알 수 있다.