지금까지 로그 추적기에 프록시를 도입하여 기존의 서비스 로직 코드들을 수정하지 않아도 되도록 하였다.
하지만 대상 클래스 수만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 것이 문제점이다. 로그 추적기를 위한 프록시 클래스들의 코드는 거의 똑같은데 말이다.
자바가 기본으로 제공하는 JDK동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어 낼 수 있다. 즉 지금처럼 프록시 클래스를 계속 만들지 않아도 되는 것이다. 프록시를 적용할 코드를 하나만 만든 뒤, 동적 프록시 기술을 사용하여 프록시 객체를 찍어내면 되는 것이다.
JDK 동적 프록시를 이해하기 위해서는 자바의 리플렉션 기술을 이해해야 한다. 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다. JDK 동적 프록시를 이해하기 위한 최소한의 리플렉션 기술을 알아보자.
리플렉션
아래와 같은 callA, callB라는 두개의 메서드를 가진 Hello클래스가 있다.
@Slf4j
static class Hello{
public String callA(){
log.info("callA");
return "A";
}
public String callB(){
log.info("callB");
return "B";
}
}
그리고 이를 다음과 같이 두 곳에서 실행한다고 가정해보자.
@Test
void reflection0(){
Hello target = new Hello();
//-----공통 로직1 시작------
log.info("start");
String result1 = target.callA(); //호출하는 메서드가 다름
//공통 로직1 종료
log.info("result1 = {}", result1);
//------------------------
//-----공통 로직2 시작------
log.info("start");
String result2 = target.callB(); //호출하는 메서드가 다름
//공통 로직1 종료
log.info("result2 = {}", result2);
//------------------------
}
실행결과:
14:54:36.816 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest - start
14:54:36.817 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest$Hello - callA
14:54:36.818 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest - result1 = A
14:54:36.819 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest - start
14:54:36.819 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest$Hello - callB
14:54:36.819 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest - result2 = B
두개의 실행부분에서 호출하는 메서드만 callA, callB로 다를 뿐이지, 똑같은 로직을 가지고 있다.
하여 이를 리플렉션을 사용하여 공통화할 수 있는데, 먼저 아직은 리플렉션으로 공통화는 하지말고, 리플렉션의 사용법을 알아보자.
@Test
void reflectionTest1() throws Exception {
//클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA");
Object result1 = methodCallA.invoke(target);
log.info("result1 = {}", result1);
//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result2 = {}", result2);
}
//실행결과는 reflectionTest0과 같음
• Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello") : 클래스 메타정보를 획득한다. 참고로 내부 클래스는 구분을 위해 $ 를 사용한다.
• classHello.getMethod("call") : 해당 클래스의 call 메서드 메타정보를 획득한다.
• methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서 methodCallA 는 Hello 클래스의 callA() 이라는 메서드 메타정보이다. methodCallA.invoke(인스턴스) 를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의 callA() 메서드를 찾아서 실행한다. 여기서는 target 의 callA() 메서드를 호출한다.
이처럼 "callA" 또는 "callB"를 Class.getMethod 메서드의 인자로 넣어 동적으로 메서드의 메타정보를 사용할 수 있는 것이다.
이제 리플렉션을 사용하여 reflectionTest1을 공통화하여 처리해보자.
이를 위해 아래와 같은 공통화 메서드를 만든다.
private void dynamicCall(Method method, Object target) throws InvocationTargetException, IllegalAccessException {
log.info("start");
Object result = method.invoke(target);
log.info("result = {}", result);
}
이를 사용하면 다음과 같다.
@Test
void reflectionTest2() throws Exception {
//클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
//실행결과는 reflectionTest0과 같음
dynaminCall 메서드의 인자로 클래스의 메타정보와 메서드의 메타정보를 넣어주면 그 안에서 동적으로 프록시 객체를 만들어 메서드를 호출해주는 것이다.
이러한 방법을 사용하면 클래스나 메서드 정보를 동적으로 변경할 수 있다.
하지만 사용하는 부분을 보았듯이 자바의 리플렉션은 문자열을 사용하기 때문에 오타가 나면 런타임 오류가 발생할 수 있다. 이러한 이유로 매우 주의하여 사용해야 한다.
이러한 단점을 해결할 수 있는 방법을 다음 포스트에서 살펴보자.
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
'Spring > spring AOP' 카테고리의 다른 글
스프링 코어2 - CGLIB (0) | 2022.01.10 |
---|---|
스프링 코어2 - JDK 동적 프록시 (0) | 2022.01.09 |
스프링 코어2 - 구체 클래스 기반 프록시 (0) | 2022.01.08 |
스프링 코어2 - 인터페이스 기반 프록시 (0) | 2022.01.08 |
스프링 코어2 - 프록시 패턴과 데코레이터 패턴 (0) | 2022.01.06 |