Q. 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
ProxyFactory
)라는 기능을 제공한다이 설정을 변경할 수도 있다. Q. 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?
Advice
라는 새로운 개념을 도입InvocationHandler
나 MethodInterceptor
를 신경 쓰지 않고 Advice
만 만들면 된다.InvocationHandler
나 MethodInterceptor
는 Advice
를 호출하게 된다.프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler
나 MethodInterceptor
를 내부에서 사용한다.
Q. 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
Pointcut
이라는 개념으로 이 문제를 일관성 있게 해결한다.Advice
는 프록시에 적용하는 부가 기능 로직이다. 이것은 JDK 동적 프록시가 제공하는
InvocationHandler
와 CGLIB가 제공하는 MethodInterceptor
의 개념과 유사하다. 둘을 개념적으로 추상화 한 것이다. 프록시 팩토리를 사용하면 둘 대신에 Advice
를 사용하면 된다
package org.aopalliance.intercept;
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
MethodInterceptor
와 이름이 같으므로 패키지 이름에 주의하자MethodInterceptor
는 Interceptor
를 상속하고 Interceptor
는 Advice
인터페이스를 상속MethodInvocation invocation
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;
}
}
}
Object result = invocation.proceed()
MethodInvocation
안에 target 정보도 들어 있다.// 인터페이스는 JDK 동적 프록시 사용
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
// 구체 클래스는 CGLIB 프록시 사용
ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
proxy.call();
new ProxyFactory(target)
proxyFactory.addAdvice(new TimeAdvice())
proxyFactory.getProxy()
: 프록시 객체를 생성하고 그 결과를 받는다.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
ProxyFactory
로 프록시를 만들었다면 AopUtils
로 어떤 프록시인지 간단하게 확인할 수 있다.proxyFactory.setProxyTargetClass(true);
스프링 부트는 AOP를 적용할 때 기본적으로
proxyTargetClass=true
로 설정해서 사용한다. 따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 프록시를 생성한다.
프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.
프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있다.
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();
new DefaultPointcutAdvisor
Advisor
인터페이스의 가장 일반적인 구현체로 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어 준다.TimeAdvice
는 Advice
의 구현체Pointcut.TRUE
: 항상 true를 반환하는 포인트컷proxyFactory.addAdvisor(advisor)
: 프록시 팩토리에 적용할 어드바이저를 지정public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
//..
}
ClassFilter
와 MethodMatcher
둘로 이루어진다. 이름 그대로 하나는 클래스가 맞는지, 하나는 메서드가 맞는지 확인할 때 사용한다. 둘 다 true로 반환해야 어드바이스를 적용할 수 있다.NameMatchMethodPointcut
: 메서드 이름 기반으로 매칭. 내부에서 PatternMatchUtils
를 사용
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();
PatternMatchUtils.*simpleMatch*
*save*
JdkRegexpMethodPointcut
: JDK 정규 표현식 기반으로 매칭TruePointcut
: 항상 참을 반환AnnotationMatchingPointcut
: 애노테이션으로 매칭AspectJExpressionPointcut
: aspectJ 표현식으로 매칭
AspectJExpressionPointcut
을 사용하게 된다.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(); ```
addAdvisor()
를 통해서 어드바이저를 등록하면 된다.스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다. 정리하면 하나의 target 에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target 마다 하나의 프록시만 생성한다.
ProxyFactory
를 통해 프록시를 빈으로 띄워야 하기 때문이다.