이전 프록시 패턴과 데코레이션 패턴을 통해 부가 기능과 메인 로직을 분리하는 패턴에 대해서 알아보았다.
AOP의 정의와 프록시/데코레이션 패턴
AOP의 정의 AOP는 관점지향프로그래밍이라 불리며 어떤 로직을 기준으로 핵심적인 로직과 부가적인 기능의 관점을 나누어 보고 그 관점을 기준으로 모듈화 한다. 흩어진 관심사를 묶고 비즈니스
programmingjun.tistory.com
프록시 패턴으로 분리는 가능하였지만 같은 부가기능을 각 로직별로 적용하고 싶다면 프록시 클래스를 하나하나 만들어 주어야 하는 불편함이 있었다. (재사용이 불가능함)
이 부분을 Java가 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스는 자바의 Reflection 기술을 통하여 프록시 객체를 동적으로 생성하도록 구성되어 있다. 이 부분에서는 콜백 패턴과 유사한 구성을 보인다.
***인프런의 김영한 강사님의 강의를 참고하여 블로그를 작성하였다.(기습숭배)***
(상황)
아래와 같이 메서드 이름을 로그로 찍고 각각 "A"와 "B" 문자열을 반환하는 메서드를 가진 클래스가 있다고 가정해 보자
@Slf4j
pubilc class Hello{
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
아래 코드는 메서드 실행 전 start라는 로그를 남기고 메서드가 끝나고 나서 그 결괏값을 로그로 담아 출력하는 부가기능이 추가된 상황이다.
@Slf4j
public class ReflectionTest {
@Test
void reflection0() {
Hello hello = new Hello();
//1 시작
log.info("start");
String result1 = hello.callA();
log.info("result1 ={}", result1);
//2 시작
log.info("start");
String result2 = hello.callB();
log.info("result2 ={}", result2);
}
}
위 상황은 이 객체(Hello)를 직접 생성하여 앞 뒤로 로그를 남기고 메서드를 호출하고 있다.
이 객체와 그 메서드에 대한 정보를 동적으로 주입해 준다면 Hello에 대한 프록시 클래스를 작성하지 않아도 될 것이다.
하물며 Hello 클래스뿐 아니라 다른 클래스일 경우에도 동적으로 객체로 만들어 넣어준면 프록시 객체를 일일이 만들지 않아도 된다.
아래 테스트 케이스 동적으로 클래스 정보와 메서드 정보를 얻어와 실행되는 패턴이다.
Class.forName() 메서드를 통하여 클래스 정보를 얻어오고
Class.getMethod() 메서드의 파라미터로 메서드 이름을 입력하여 메서드 정보를 얻어왔다.
그리고 앞뒤로 로그는 부가기능 즉, AOP 대상이 되는 코드들이다.
@Test
void reflection1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdinamic.ReflectionTest$Hello");
Hello hello = new Hello();
//callA 메서드 정보 얻기
log.info("start");
Method callA = classHello.getMethod("callA");
Object result1 = callA.invoke(hello);
log.info("result1 ={}", result1);
//callB 메서드 얻기
log.info("start");
Method callB = classHello.getMethod("callB");
Object result2 = callB.invoke(hello);
log.info("result2 ={}",result2);
}
위 코드를 보면 결과적으로는 Hello라는 객체를 만들지 않았는가?라고 생각 할 수 있지만
이 코드에서는 hello.callA()를 직접 호출하지 않고
hello 클래스에 대한 정보와 callA()라는 매서드의 정보만 얻어 오고 있다.
그리고 메서드 실행 전 후의 AOP로 분리될 코드를 메서드로 분리한다면 아래와 같은 메서드로 작성될 수 있다.
부가 가능을 담당하는 어드바이스 역할이라고 할 수 있다.
private void dynamicCall(Method method, Object target) throws InvocationTargetException, IllegalAccessException {
log.info("start");
Object result = method.invoke(target);
log.info("result={}", result);
}
동적으로 실행할 것이기 때문에 어떤 객체도 들어올 수 있게
객체를 최상위 타입인 Object를 파라미터로 받고 실행할 메서드를 파라미터로 받는다.
뒤의 Exception은 타입이 맞지 않거나 타겟이 잘못 들어왔을 때 발생할 수 있는 예외들이다.
그러면 아래와 같이 동적으로 CallBack 하여 AOP를 적용할 모든 클래스에 Proxy 클래스를 만드는 것이 아니라 동적으로 클래스 정보와 메서드 이름을 받아와 사용할 수 있다. 더 나아가 Hello라는 클래스만이 아닌 다른 클래스에도 해당 부가기능을 적용시키기 용이하다. (Proxy 클래스를 모두 만들지 않아도 됨)
@Test
void reflection2() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdinamic.ReflectionTest$Hello");
Hello hello = new Hello();
//callA 메서드 정보 얻기
Method callA = classHello.getMethod("callA");
dynamicCall(callA, hello);
//callB 메서드 얻기
Method callB = classHello.getMethod("callB");
dynamicCall(callB, hello);
}
위와 같은 형식의 동적 프록시 기능을 사용할 수 있도록
JDK 동적 프록시 는 InvocationHandler를
Cglib는 MethodInterceptor 인터페이스를 제공한다.
JDK 동적 프록시의 InvocationHandler
@RequiredArgsConstructor
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("time proxy 실행");
Long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
Long endTime = System.currentTimeMillis();
Long resultTime = endTime - startTime;
log.info("time proxy 종료 resultTime ={}", resultTime);
return result;
}
}
의존 관계가 주입되는 그림은 아래 테스트 코드를 통해 확인할 수 있다.
@Slf4j
public class JdkDynamicProxyTest {
@Test
void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
/*
time proxy 실행
A 호출
time proxy 종료 resultTime =1
targetClass=class hello.proxy.jdkdinamic.code.AImpl
proxyClass=class com.sun.proxy.$Proxy9
*/
Cglib의 MethodInterceptor
@RequiredArgsConstructor
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("time proxy 실행");
Long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(target, args);
Long endTime = System.currentTimeMillis();
Long resultTime = endTime - startTime;
log.info("time proxy 종료 resultTime ={}", resultTime);
return result;
}
}
의존 관계가 주입되는 그림은 아래 테스트 코드를 통해 확인할 수 있다.
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create();
log.info("target class={}", target.getClass());
log.info("proxy class={}", proxy.getClass());
proxy.call();
}
}
/*
target class=class hello.proxy.common.service.ConcreteService
proxy class=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
time proxy 실행
ConcreteService 호출
time proxy 종료 resultTime =19
*/
의존관계를 주입해주는 방식과 AOP의 어드바이스(부가기능)의 상속 Interface가 다소 차이가 있다.
그렇다면 인터페이스 기반과 구체 클래스 기반의 Proxy를 작성할 때 각다른 설정으로 동적 프록시를 생성해야하는 것일까?
그렇지 않다. Spring은 이 두 기능을 통합하는 ProxyFactory 기능을 지원한다. Bean에 등록된 정보를 바탕으로 그 기반에 맞는 동적 프록시를 생성해 준다.
코드 출처
인프런 / 김영한 / 스프링 핵심원리, 고급편
'Java' 카테고리의 다른 글
객체지향의 5가지 원칙 (SOLID) (2) | 2023.12.22 |
---|---|
switch case문 (Java 14~) (1) | 2023.12.06 |
쓰레드 (0) | 2022.12.30 |