지난 포스팅에서 프록시 팩토리에 대해서 알아보았다. 프록시 팩토리는 JDK 동적 프록시와 CGLIB 상관 없이 동적으로 프록시를 만들어 준다. 이번 포스팅에서는 AOP에서 중요한 개념인 포인트컷, 어드바이스, 어드바이저에 대해서 알아보고 이 개념을 기반으로 프록시 팩토리를 로그 추적기에 적용해보도록 하자.
스프링에 대한 이론을 공부할 때 스프링 AOP에 대해서 알게 되었는데, 그때는 프록시 같은 개념에 대해서도 알지 못하던 때라 아무리 봐도 이해가 잘 되질 않았다. 하지만 중요한 개념이므로 확실하게 짚고 넘어가자.
어떤 부가 기능(어드바이스)을 어디에(포인트컷) 적용할지 알고 있는 것이 어드바이저이다.
코드로 확인해보자.
@Test
void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
위의 예제에서는 모든 메서드에 어드바이스가 적용되도록 설정하였다.
이번에는 포인트컷을 직접 만들어 save메서드에만 어드바이스가 적용되도록 해보자.
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
// 포인트 컷을 따로 만들려면 MethodMatcher를 직접 생성해줘야 함
// 메서드를 비교하는 기능
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
// MethodMatcher 직접 생성
static class MyMethodMatcher implements MethodMatcher{
private String matchName = "save";
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(matchName);
log.info("포인트컷 호출 method={} targetClass={}", method.getName(), targetClass);
log.info("포인트컷 결과 result={}",result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
Pointcut
MethodMatcher
@Test
@DisplayName("직접 만든 포인트컷")
void advisorTest2() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
이상으로 직접 포인트컷을 생성해서 적용까지 해봤다.
하지만 항상 그렇듯 스프링에서 이미 대부분의 포인트컷을 제공한다.
@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
스프링에서는 많은 수의 포인트컷을 제공하는데, 그 중 일부만 알아보면
실무에서는 AspectJExpressionPointcut를 가장 많이 사용한다고 한다.
이것으로 포인트컷에 대해서 알아보았다.
앞에서 하나의 어드바이저는 하나의 포인트컷, 하나의 어드바이스를 가지고 있다고 하였다.
그런데 하나의 target에 여러개의 어드바이저를 적용하려면 어떻게 해야할까?
여러 어드바이저를 적용하는 방법은 여러개의 프록시를 만드는 방법도 있을 것이다. 그리고 그렇게 한다고 했을 때 객체관의 관계는 아래와 같다.
이 경우 어드바이스의 개수만큼 프록시를 생성해야 한다.
스프링에서는 이 문제에 대해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 해준다.
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
// client -> proxy2(advisor2) -> proxy1(advisor1) -> target
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
// 프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
// 어드바이저 추가
// 어드바이스는 추가된 순서대로 실행
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
// 실행
proxy.save();
}
그럼 이제 로그 추적기에 프록시 팩토리를 직접 적용해보자.
public class LogTraceAdvice implements MethodInterceptor {
private final LogTrace logTrace;
public LogTraceAdvice(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TraceStatus status = null;
try {
Method method = invocation.getMethod();
String message = method.getDeclaringClass().getSimpleName() + "."
+ method.getName() + "()";
status = logTrace.begin(message);
// 로직 호출
Object result = invocation.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
ProxyFactory proxyFactory = new ProxyFactory(orderController);
proxyFactory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) proxyFactory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
ProxyFactory proxyFactory = new ProxyFactory(orderService);
proxyFactory.addAdvisor(getAdvisor(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) proxyFactory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
ProxyFactory proxyFactory = new ProxyFactory(orderRepository);
proxyFactory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) proxyFactory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
private Advisor getAdvisor(LogTrace logTrace) {
// pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "save*", "order*");
// advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
// advisor
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
@Bean
public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
ProxyFactory proxyFactory = new ProxyFactory(orderController);
proxyFactory.addAdvisor(getAdvisor(logTrace));
OrderControllerV2 proxy = (OrderControllerV2) proxyFactory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
첫번째는 JDK 동적 프록시로, 두번째는 CGLIB로 동적 프록시를 생성해준다.
다만, 인터페이스에 구현체를 넣어 프록시 팩토리에 넘기느냐, 구현체를 프록시 팩토리에 넘기느냐 하는 차이만 있을뿐, 프록시 팩토리를 사용하는 방식은 완벽하게 똑같다. 이처럼 프록시 팩토리를 활용하면, 동적 프록시를 생성하는 기술을 신경쓰지 않고, 단지 프록시 팩토리를 활용해 동적으로 프록시를 생성할 수 있게 되었다.
이것으로 AOP에서 중요한 개념인 포인트컷, 어드바이스, 어드바이저에 대해서 알아보았다.
포인트컷은 2가지 역할을 한다.
이를 기반으로 어드바이스를 적용하는데,
어드바이스는 적용하고자 하는 부가기능(여기서는 로그 추적기)을 말한다.
어드바이저는 하나의 포인트컷 + 하나의 어드바이스를 가지고 있는 것을 말하는데, 어드바이저를 가지고 있다면 어떤 포인트컷과 어떤 어드바이스를 적용해야할지 알 수 있다. 따라서 프록시 팩토리에는 어드바이저를 지정해주는 것만으로 원하는 어드바이저를 적용할 수 있다.
여기서 기억해둬야 할 것은, 하나의 타겟에는 하나의 프록시만 생성한다는 것이다. 타겟에 여러 개의 어드바이저를 적용한다고 하더라도 스프링에서는 타겟에 하나의 프록시만을 생성하고, 하나의 프록시에 여러 어드바이스를 적용하도록 지원한다.
프록시 팩토리를 사용한 결과 개발자는 동적 프록시 구현 기술을 신경쓰지 않고 프록시를 생성할 수 있게 되었다.
하지만 프록시 팩토리에서도 단점이 있다.
설정 로직이 복잡해진다.
각 타겟에 프록시를 생성할 때마다 프록시 팩토리를 생성해줘야 하고, 그 때마다 어드바이저를 생성해서 주입하는 등의 로직을 작성해야 한다.
타겟이 많아질수록 설정 로직도 그만큼 많아지게 되는 것이다.
컴포넌트 스캔
@Component를 이용해 빈으로 등록하는 객체의 경우 어플리케이션이 구축되는 과정에서 컨테이너에 빈으로 등록된 상태이기 때문에 앞서 확인한 방법으로는 프록시 적용이 불가능하다.
이런 문제를 해결하려면 어떻게 해야할까?
빈 후처리기를 사용하면 된다.
다음 포스팅에서는 빈 후처리기에 대해 알아보자.
출처 : 김영한 - 스프링 핵심 원리 고급편