포인트컷(
Pointcut
): 어디에 프록시를 적용할지 판단하는 필터링 로직
주로 클래스와 메서드 이름으로 필터링 한다.
어드바이스(
Advice
): 프록시가 호출하는 실제 로직
이전포스팅의 마지막에서 본 것 처럼 프록시가 호출하는 실제 로직이다.
어드바이저(
Advisor
): 1포인트컷 + 1어드바이스
단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.
*이해를 돕기 위한 그림으로, 실제 구현은 살짝 다를 수 있다.
@Slf4j
public class AdvisorTest {
@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();
}
}
일반적으로 new DefaultPointcutAdvisor를 통해 어드바이저를 생성한다.
Pointcut.TRUE를 통해 모든 대상에 어드바이스(new TimeAdvice)를 적용한다.
그렇게 생성한 어드바이저를 proxyFactory.addAdvisor 를 통해 주입한다.
참고로 이전 포스팅 의 마지막에서는 proxyFactory.addAdvice(new TimeAdvice()); 를 통해
바로 어드바이스를 주입했는데, addAdvice는 편의성 메소드로,
proxyFactory.addAdvice(new TimeAdvice());
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
결과적으로 위 두개는 동일하게 동작한다.
#save() 호출
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0ms
#find() 호출
TimeAdvice - TimeProxy 실행
ServiceImpl - find 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
위에서 포인트컷은 프록시 적용 여부
라고 소개했다.
포인트컷을 통해 save() 메소드에만 프록시를 적용해보자.
(find에 적용 안함)
포인트컷은 크게 ClassFilter
와 MethodMatcher
둘로 이루어진다.
둘다 true
로 반환해야 어드바이스를 적용할 수 있다.
스프링이 제공하는 포인트 컷도 있지만, 직접 만들어보자.
static class MyPointcut implements Pointcut {
@Override
// 클래스가 맞는지 확인
// 클래스는 따로 필터걸지 않는다. (ClassFilter.TRUE)
public ClassFilter getClassFilter() {return ClassFilter.TRUE;}
@Override
// 메소드가 맞는지 확인
public MethodMatcher getMethodMatcher() {return new MyMethodMatcher();}
}
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) {
throw new UnsupportedOperationException();
}
}
isRuntime이 true이면, 아래쪽 matches(Method, Class<?>, Object...) 메소드를 사용한다.
-> 메소드에 넘어오는 파라미터값을 동적으로 활용할 수 있게된다.
isRuntime이 false이면, 위쪽 matches(Method, Class<?>) 메소드를 사용한다.
-> 파라미터를 동적으로 활용하지 않고, 정적인 클래스 정보만 활용한다.
-> 스프링 내부적으로 캐싱을 통해 성능이 향상된다.
@Slf4j
public class AdvisorTest {
@Test
void advisorTest2() {
ServiceImpl 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();
}
}
#save() 호출
AdvisorTest - 포인트컷 호출 method=save targetClass=class hello.proxy.common.service.ServiceImpl
AdvisorTest - 포인트컷 결과 result=true
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
#find() 호출
AdvisorTest - 포인트컷 호출 method=find targetClass=class
hello.proxy.common.service.ServiceImpl
AdvisorTest - 포인트컷 결과 result=false
ServiceImpl - find 호출
save에만 TimeProxy 실행
이 출력된 것을 알 수 있다.
스프링은 무수히 많은 포인트컷을 제공한다. 대표적인 몇가지만 알아보자.
NameMatchMethodPointcut
: 메서드 이름 기반 매칭.PatternMatchUtils
를 사용한다. (*xxx*
같은 표현도 가능하다.)JdkRegexpMethodPointcut
: JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.TruePointcut
: 항상 참을 반환한다.AnnotationMatchingPointcut
: 애노테이션으로 매칭한다.AspectJExpressionPointcut
: aspectJ 표현식으로 매칭한다.@Slf4j
public class AdvisorTest {
@Test
void advisorTest3() {
ServiceImpl target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// 스프링이 제공하는 NameMatchMethodPointcut 사용
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();
}
}
#save() 호출
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
#find() 호출
ServiceImpl - find 호출
1개의 target에 여러 Advisor를 적용하려면 어떻게 해야할까?
public class MultiAdvisorTest {
@Test
void multiAdvisorTest1() {
//client -> proxy2(advisor2) -> proxy1(advisor1) -> target
//프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy1 = (ServiceInterface)proxyFactory1.getProxy();
//프록시2 생성, target -> proxy1 입력
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface)proxyFactory2.getProxy();
//실행
proxy2.save();
}
@Slf4j
static class Advice1 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice1 호출");
return invocation.proceed();
}
}
@Slf4j
static class Advice2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice2 호출");
return invocation.proceed();
}
}
}
런타임 시점에서 아래와 같은 구조를 가진다.
MultiAdvisorTest$Advice2 - advice2 호출
MultiAdvisorTest$Advice1 - advice1 호출
ServiceImpl - save 호출
기대했던 대로 동작하는 것을 확인할 수 있다.
하지만, 적용할 어드바이저 개수만큼 프록시를 생성하는 것은 너무 불편하다.
스프링은 이러한 문제를 해결하기 위해, 1개의 프록시에 여러 어드바이저를 적용할 수 있게 만들어두었다.
public class MultiAdvisorTest {
@Test
void multiAdvisorTest2() {
//proxy -> advisor2 -> advisor1 -> target
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface)proxyFactory1.getProxy();
//실행
proxy.save();
}
}
런타임 시점에서 아래와 같은 구조를 가진다.
@Slf4j
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 factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1)factory.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 factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV1 proxy = (OrderServiceV1)factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1)factory.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*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
NameMatchMethodPointcut
포인트컷은 위에서 언급한 대로 PatternMatchUtils
을 사용하기 때문에, *
을 통한 매칭이 가능하다.request*
, order*
, save*
: 각 단어로 시작하는 메서드에 어드바이스를 적용한다.noLog()
메서드에는 어드바이스를 적용하지 않는다.@Import(ProxyFactoryConfigV1.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
어플리케이션 실행 시
ProxyFactory proxy=class com.sun.proxy.$Proxy50,
target=class ...v1.OrderRepositoryV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy52,
target=class ...v1.OrderServiceV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy53,
target=class ...v1.OrderControllerV1Impl
v1은 인터페이스를 사용하기 때문에 JDK 동적 프록시가 생성됐음을 알 수 있다.
http://localhost:8080/v1/request?itemId=hello
[aaaaaaaa] OrderControllerV1.request()
[aaaaaaaa] |-->OrderServiceV1.orderItem()
[aaaaaaaa] | |-->OrderRepositoryV1.save()
[aaaaaaaa] | |<--OrderRepositoryV1.save() time=1002ms
[aaaaaaaa] |<--OrderServiceV1.orderItem() time=1002ms
[aaaaaaaa] OrderControllerV1.request() time=1003ms
@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
@Bean
public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV2 proxy = (OrderControllerV2)factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV2 proxy = (OrderServiceV2)factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
OrderRepositoryV2 orderRepository = new OrderRepositoryV2();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV2 proxy = (OrderRepositoryV2)factory.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*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
@Import(ProxyFactoryConfigV2.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
어플리케이션 실행 시
ProxyFactory proxy=class hello.proxy.app.v2.OrderRepositoryV2$
$EnhancerBySpringCGLIB$$594e4e8, target=class
hello.proxy.app.v2.OrderRepositoryV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderServiceV2$
$EnhancerBySpringCGLIB$$59e5130b, target=class hello.proxy.app.v2.OrderServiceV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderControllerV2$
$EnhancerBySpringCGLIB$$79c0b9e, target=class
hello.proxy.app.v2.OrderControllerV2
v2는 구체클래스를 사용하기 때문에 CGLIB 프록시가 적용됐음을 알 수 있다.
http://localhost:8080/v2/request?itemId=hello
[bbbbbbbb] OrderControllerV2.request()
[bbbbbbbb] |-->OrderServiceV2.orderItem()
[bbbbbbbb] | |-->OrderRepositoryV2.save()
[bbbbbbbb] | |<--OrderRepositoryV2.save() time=1001ms
[bbbbbbbb] |<--OrderServiceV2.orderItem() time=1003ms
[bbbbbbbb] OrderControllerV2.request() time=1005ms
개발자들은 프록시 팩토리를 통해 인터페이스, 구체클레스 여부에 상관 없이 프록시를 편리하게 생성할 수 있게 되었다.
어드바이저, 어드바이스, 포인트컷 을 통해 역할을 분리함으로써, 어디에
, 어떤 로직을
적용할 지 명확하게 됐다.
하지만 아직 남은 문제가 있다.
ProxyFactoryConfigV1
, ProxyFactoryConfigV2
와 같은 설정 파일을
프록시를 적용하고자 하는 스프링 빈만큼 생성해주어야 한다.
실제 객체를 초기에 스프링 컨테이너에 빈으로 등록하는 컴포넌트 스캔은, 지금까지 학습한 방법으로는 프록시를 적용할 수 없다.
우리가 지금까지 학습한 방법은 ProxyFactoryConfig..
에서 한 것 처럼, 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록하는 방법이다.
스프링이 스프링 컨테이너에 빈을 등록하기 이전에 객체를 조작
하거나 객체를 바꿔치기
하는 빈 후처리기
를 통해 해결할 수 있다.