@Slf4j
public class ReflectionTest {
@Test
void reflection0() {
final Hello target = new Hello();
//공통 로직1 시작
log.info("start");
final String result1 = target.callA();
log.info("result = {}", result1);
//공통 로직1 종료
//공통 로직2 시작
log.info("start");
final String result2 = target.callB();
log.info("result = {}", result2);
//공통 로직2 종료
}
@Slf4j
static class Hello{
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
}
- 위의 코드를 보면 공통 로직1과 공통 로직2는 호출하는 메소드만 다르고 전체 코드 흐름이 완전히 같다.
- 먼저 start 로그를 출력한다.
- 어떤 메소드를 호출한다.
- 메소드의 호출 결과를 로그로 출력한다.
- 여기서 공통 로직1과 공통 로직2를 하나의 메소드로 뽑아서 합칠 수 있을까?
- 쉬워 보이지만 메소드로 뽑아서 공통화하는 것이 생각보다 어렵다. 왜냐하면 중간에 호출하는 메소드가 다르기 때문이다.
- 호출하는 메소드인 target.callA(), target.callB() 이 부분만 동적으로 처리할 수 있다면 문제를 해결할 수 있을듯 하다.
이럴 때 사용하는 것이 바로 리플렉션(Reflection) 이다.
리플렉션은 클래스나 메소드의 메타정보를 사용해서 동적으로 호출하는 메소드를 변경할 수 있다.
@Test
void reflection1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//클래스 정보
final Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
final Hello target = new Hello();
//callA 메소드 정보
final Method methodCallA = classHello.getMethod("callA");
final Object result1 = methodCallA.invoke(target);
log.info("result1 = {} ", result1);
//callB 메소드 정보
final Method methodCallB = classHello.getMethod("callB");
final Object result2 = methodCallB.invoke(target);
log.info("result2 = {} ", result2);
}
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello") : 클래스 메타 정보를 획득한다. 내부 클래스는 구분을 위해 $를 사용한다.
Method methodCallA = classHello.getMethod("callA") : 해당 클래스의 call 메소드 메타정보를 획득한다.
Object result1 = methodCallA.invoke(target) : 획득한 메소드 메타정보로 실제 인스턴스의 메소드를 호출한다. 여기서 methodCallA는 Hello 클래스의 callA()라는 메소드 메타정보다. methodCallA.invoke(인스턴스)를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의 callA() 메소드를 찾아서 실행한다. 여기서는 target의 callA() 메소드를 호출한다.
이렇게 메소드 정보를 획득해서 메소드를 호출하면 클래스나 메소드 정보를 동적으로 변경할 수 있다.
@Test
void reflection2() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
final Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
final Hello target = new Hello();
//callA 메소드 정보
final Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
//callB 메소드 정보
final Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws InvocationTargetException, IllegalAccessException {
log.info("start");
final Object result = method.invoke(target);
log.info("result = {}", result);
}
start
callA
result = A
start
callB
result = B
dyncamicCall(Method method, Object object)
- 공통 로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 로직이다.
- Method method : 첫 번째 파라미터는 호출할 메소드 정보가 넘어온다. 이것이 핵심이다. 기존에는 메소드 이름을 직접 호출했지만, 이제는 Method라는 메타정보를 통해서 호출할 메소드 정보가 동적으로 제공된다.
- Object object : 실제 실행할 인스턴스 정보가 넘어온다. 타입이 Object 라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다. 물론 method.invoke(target)를 사용할 때 호출할 클래스와 메소드 정보가 서로 다르면 예외가 발생한다.
정적인 target.callA(), target.callB() 코드를 리플렉션을 사용하여 Method라는 메타정보로 추상화 하였다.
덕분에 공통 로직을 만들 수 있게 되었다.
리플렉션을 사용하면 클래스와 메소드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다.
하지만 리플렉션 기술을 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
예를 들어서 getMethod("callA") 안에 들어가는 문자를 실수로 getMethod("callZ")로 작성해도 컴파일 오류가 발생하지 않는다.
대신 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임에 오류가 발생한다.
가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다.
따라서 리플렉션은 일반적으로 사용하면 안된다.