자바에서 Reflection은 런타임에 클래스를 조사하고, 그 클래스의 속성(필드, 메서드 등)이나 생성자를 동적으로 접근할 수 있는 기능을 제공하는 메커니즘입니다.
즉, 컴파일 시점에 어떤 클래스의 구조를 알 수 없는 상황에서 런타임에 그 클래스에 대한 정보를 알아내고 조작할 수 있는 방법입니다.
Class<?> clazz = Class.forName("패키지.클래스이름");
Field field = clazz.getDeclaredField("필드이름");
field.setAccessible(true); // private 필드에 접근 가능하게 설정
field.set(인스턴스, 값); // 필드 값 설정
Method method = clazz.getDeclaredMethod("메서드이름", 파라미터타입들);
method.setAccessible(true); // private 메서드에 접근 가능하게 설정
method.invoke(인스턴스, 인자들); // 메서드 호출
Constructor<?> constructor = clazz.getDeclaredConstructor(파라미터타입들);
constructor.setAccessible(true); // private 생성자에 접근 가능하게 설정
Object instance = constructor.newInstance(인자들); // 인스턴스 생성
프레임워크: Spring, Hibernate 등 많은 자바 프레임워크는 Reflection을 사용하여 객체를 동적으로 생성하고 관리합니다. 특히 의존성 주입(DI), AOP(Aspect-Oriented Programming)와 같은 기능에서 Reflection이 많이 사용됩니다.
동적 프록시 생성: 런타임에 동적으로 클래스나 메서드의 동작을 변경하거나 확장할 때 유용합니다.
유닛 테스트: private 메서드나 필드에 접근해야 할 때 Reflection을 통해 테스트를 수행할 수 있습니다.
성능 저하: Reflection은 런타임에 동작하므로 컴파일 타임에 검증이 되지 않으며, 성능 면에서 일반적인 메서드 호출보다 느립니다.
안전성 문제: Reflection을 사용하면 private 필드나 메서드에 접근할 수 있으므로, 클래스의 캡슐화 원칙을 깨뜨릴 수 있습니다.
컴파일 타임 타입 체크 불가: Reflection을 사용하면 코드에서의 타입 체크가 런타임에 이루어지므로, 컴파일 시점에 발생할 수 있는 오류를 발견하기 어렵습니다.
간단한 예제
import java.lang.reflect.*;
class Example {
private String message;
private Example(String message) {
this.message = message;
}
private void printMessage() {
System.out.println(message);
}
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// 클래스 정보 얻기
Class<?> clazz = Class.forName("Example");
// private 생성자로 인스턴스 생성
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance("Hello Reflection!");
// private 메서드 호출
Method method = clazz.getDeclaredMethod("printMessage");
method.setAccessible(true);
method.invoke(instance); // 출력: Hello Reflection!
}
}
위 코드는 Example 클래스의 private 생성자와 private 메서드를 Reflection을 이용해 접근하고 실행하는 예제입니다.
Reflection을 사용할 때는 항상 주의가 필요하며, 성능에 민감하거나 보안이 중요한 시스템에서는 적절히 사용해야 합니다.
Reflection은 런타임에 메타데이터를 조회하고 메서드를 호출하기 때문에 일반적인 코드보다 성능이 떨어집니다. 이를 최소화하는 몇 가지 방법은 다음과 같습니다:
Reflection을 통해 얻은 메서드, 필드 정보는 Method, Field, Constructor 객체로 반환되는데, 이 객체들을 재사용 가능하게 캐싱해두면 성능을 개선할 수 있습니다.
예를 들어, 클래스의 메서드 정보를 여러 번 조회할 필요가 있다면, 첫 번째 조회 이후 결과를 캐시에 저장하고 이후에는 캐시된 데이터를 사용하는 방식입니다.
private static final Map<String, Method> methodCache = new HashMap<>();
public static Method getCachedMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
String key = clazz.getName() + "." + methodName;
return methodCache.computeIfAbsent(key, k -> {
try {
return clazz.getDeclaredMethod(methodName, parameterTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
}
Spring 프레임워크는 매우 광범위하게 Reflection을 사용합니다. 주요 사례를 몇 가지 소개하겠습니다:
예를 들어, @Autowired로 의존성을 주입할 때, Spring은 해당 필드나 생성자를 Reflection으로 탐색하여 주입할 객체를 찾아 넣습니다.
public class MyService {
@Autowired
private UserRepository userRepository; // Reflection을 통해 주입
}
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("Method 호출: " + method.getName());
}
}
Reflection을 사용하지 않고도 동적 메서드 호출을 구현할 수 있는 방법들은 여러 가지가 있습니다. 몇 가지 대안을 소개합니다:
interface Operation {
void execute();
}
class PrintOperation implements Operation {
public void execute() {
System.out.println("Print 작업 수행");
}
}
// 미리 Operation을 구현한 클래스들을 주입받아 사용
interface Operation {
void execute();
}
public class DynamicInvoker {
public void invoke(Operation operation) {
operation.execute();
}
}
public static void main(String[] args) {
DynamicInvoker invoker = new DynamicInvoker();
invoker.invoke(() -> System.out.println("동적 호출"));
}
InvocationHandler handler = (proxy, method, args) -> {
System.out.println("동적 프록시 호출");
return null;
};
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class<?>[]{MyInterface.class},
handler
);
proxy.myMethod();
이 방법들은 Reflection의 성능 문제나 안전성 문제를 피하면서도 동적 메서드 호출을 구현할 수 있는 대안입니다. 상황에 맞게 적절한 방법을 선택하는 것이 중요합니다.