Spring AOP - 포인트컷, 어드바이스, 어드바이저

조갱·2024년 6월 9일
0

스프링 강의

목록 보기
18/23

포인트컷, 어드바이스, 어드바이저

포인트컷(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에 적용 안함)

포인트컷은 크게 ClassFilterMethodMatcher 둘로 이루어진다.
둘다 true 로 반환해야 어드바이스를 적용할 수 있다.

스프링이 제공하는 포인트 컷도 있지만, 직접 만들어보자.

Pointcut

static class MyPointcut implements Pointcut {
    @Override
    // 클래스가 맞는지 확인
    // 클래스는 따로 필터걸지 않는다. (ClassFilter.TRUE)
    public ClassFilter getClassFilter() {return ClassFilter.TRUE;}

    @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) {
        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 표현식으로 매칭한다.
    실무에서는 AspectJ 표현식이 가장 많이 사용된다.
    AspectJ 표현식이 가장 정밀하게 필터가 가능하기 때문이다.
    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)

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 호출

기대했던 대로 동작하는 것을 확인할 수 있다.
하지만, 적용할 어드바이저 개수만큼 프록시를 생성하는 것은 너무 불편하다.

여러 어드바이저 함께 적용 (2)

스프링은 이러한 문제를 해결하기 위해, 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();
    }
}

런타임 시점에서 아래와 같은 구조를 가진다.

프록시 팩토리

인터페이스가 있는 클래스에 적용 (JDK 동적 프록시)

LogTraceAdvice

@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;
        }
    }
}

ProxyFactoryConfigV1

@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() 메서드에는 어드바이스를 적용하지 않는다.

Application

@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

구체 클래스에 적용 (CGLIB 프록시)

ProxyFactoryConfigV2

@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);
    }
}

Application

@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.. 에서 한 것 처럼, 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록하는 방법이다.

해결 방법

스프링이 스프링 컨테이너에 빈을 등록하기 이전에 객체를 조작하거나 객체를 바꿔치기 하는 빈 후처리기를 통해 해결할 수 있다.

profile
A fast learner.

0개의 댓글