Advisor

현시기얌·2021년 11월 24일
0

AOP

목록 보기
6/19

예제 코드

    @Test
    void advisorTest1() {
        //given
        final ServiceInterface target = new ServiceImpl();
        final ProxyFactory proxyFactory = new ProxyFactory(target);
        final DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        final ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        //when
        proxy.save();
        proxy.find();
        //then

    }
  • new DefaultPointcutAdvisor : Advisor 인터페이스의 가장 일반적인 구현체이다.
    생성자를 통해 하나의 포인트 컷과 하나의 어드바이스를 넣어주면 된다.
  • Pointcut.TRUE : 항상 true를 반환하는 포인트 컷이다.
  • proxyFactory.addAdvisor(advisor) : 프록시 팩토리에 적용할 어드바이저를 지정한다.
    어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있다.
    따라서 어디에 어떤 부가 기능을 적용해야 할지 어드바이스 하나로 알 수 있다.
    프록시 팩토리를 사용할 때 어드바이저는 필수이다.

프록시 팩토리 - 어드바이저 관계

프록시 팩토리는 어드바이저를 알고 있고 어드바이저는 포인트컷과 어드바이스를 알고 있다.

포인트컷 적용하기

위의 코드는 Pointcut.TRUE 해서 항상 어드바이스 로직을 실행하도록 하였지만 포인트컷을 직접 커스텀할 수 있다.

Pointcut 관련 인터페이스

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로 반환해야 Advice를 적용할 수 있다.

    static class MyPointcut implements Pointcut{

        @Override
        public ClassFilter getClassFilter() {
            return ClassFilter.TRUE;
        }

        @Override
        public MethodMatcher getMethodMatcher() {
            return new MyMethodMatcher();
        }
    }

    static class MyMethodMatcher implements MethodMatcher{

        private static final String matchName = "save";

        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            final 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();
        }
    }

직접 커스텀한 포인트 컷이다. Pointcut 인터페이스를 구현한다.
클래스 필터는 항상 true를 반환하도록 했고, 메소드 비교 기능은 MyMethodMatcher를 사용한다.

MyMethodMatcher

직접 구현한 MethodMatcher다. MethodMatcher 인터페이스를 구현한다.

  • matches() : 이 메소드에 method, targetClass 정보가 넘어온다. 이 정보로 Advice를 적용할지 말지 판단할 수 있다.
  • isRuntime(), mathces(... args) : isRuntime() 값이 참이면 matches(... args) 메소드가 대신 호출된다.
    동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다.
    • isRuntime()이 false인 경우 클래스의 정적 정보만 사용하기 떄문에 스프링이 내부에서 캐싱을 통해 성능이 향상 가능하다.
      isRuntime()이 true인 경우 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.
 @Test
    @DisplayName("직접 만든 포인트컷")
    void advisorTest2() {
        //given
        final ServiceInterface target = new ServiceImpl();
        final ProxyFactory proxyFactory = new ProxyFactory(target);
        final DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        final ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        //when
        proxy.save();
        proxy.find();
        //then

    }

실행 결과

포인트컷 호출 method = save targetClass = class hello.proxy.common.service.ServiceImpl
포인트컷 결과 result = true
TimeProxy 실행
save 호출
TimeProxy 종료 resultTime = 0
포인트컷 호출 method = find targetClass = class hello.proxy.common.service.ServiceImpl
포인트컷 결과 result = false
find 호출

  1. 클라이언트가 프록시의 save()를 호출한다.
  2. 포인트컷에 Service 클래스의 save()메소드에 Advice를 적용해도 될지 물어본다.
  3. 포인트컷이 true를 반환한다. 따라서 Advice를 호출해서 부가 기능을 적용한다.
  4. 이후 실제 인스턴스의 save()를 호출한다.

  1. 클라이언트가 프록시의 find()를 호출한다.
  2. 포인트컷에 Service 클래스의 find() 메소드에 Advice를 적용해도 될지 물어본다.
  3. 포인트컷이 false를 반환한다. 따라서 Advice를 호출하지 않고 부가 기능도 적용되지 않는다.
  4. 실제 인스턴스를 호출한다.

스프링이 제공하는 포인트컷

스프링은 많은 포인트컷을 제공한다.

    @Test
    @DisplayName("스프링이 제공하는 포인트컷")
    void advisorTest3() {
        //given
        final ServiceInterface target = new ServiceImpl();
        final ProxyFactory proxyFactory = new ProxyFactory(target);
        final NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedName("save");
        final DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        final ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        //when
        proxy.save();
        proxy.find();
        //then

    }
  • NameMatchMethodPointcut : 메소드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils를 사용한다.
  • JdkRegexMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
  • TruePointcut : 항상 참을 반환한다.
  • AnnotationMatchingPointcut : 애노테이션으로 매칭한다.
  • AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.

가장 중요한 것은 aspectJ 표현식

여러 Advisor 함께 적용

    @Test
    @DisplayName("여러 프록시")
    void multiAdvisorTest1() {
        // proxy2(advisor2) -> proxy1(advisor1) -> target

        // 프록시1 생성
        final ServiceInterface target = new ServiceImpl();
        final ProxyFactory proxyFactory1 = new ProxyFactory(target);
        final DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        proxyFactory1.addAdvisor(advisor1);
        final ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

        // 프록시2 생성
        final ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
        final DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
        proxyFactory2.addAdvisor(advisor2);
        final 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();
        }
    }

실행 결과

advice2 호출
advice1 호출
save 호출

여러 프록시의 문제

만약 적용해야 하는 Advisor가 10개라면 10개의 프록시를 생성해야 한다.

하나의 프록시, 여러 어드바이저

스프링은 이 문제를 해결하기 위해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 만들어두었다.

    @Test
    @DisplayName("하나의 프록시 여러 어드바이저")
    void multiAdvisorTest2() {
        // client -> proxy -> advisor2 -> advisor1 -> target

        final DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        final DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());


        // 프록시 생성
        final ServiceInterface target = new ServiceImpl();
        final ProxyFactory proxyFactory = new ProxyFactory(target);

        proxyFactory.addAdvisor(advisor2);
        proxyFactory.addAdvisor(advisor1);

        final ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        //실행
        proxy.save();
    }

실행 결과

advice2 호출
advice1 호출
save 호출

프록시 팩토리에 원하는 만큼 addAdvisor()를 통해서 어드바이저를 등록하면 된다.
등록하는 순서대로 advisor가 호출된다.

중요 포인트

스프링 AOP를 처음 공부하거나 사용하면, AOP 적용 수 만큼 프록시가 생성된다고 착각하게 된다.
스프링은 AOP를 적용할 때 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의 target에 여러 AOP가 동시에 적용되어도 스프링 AOP는 target 마다 하나의 프록시만 생성한다.

정리

프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다.
추가로 Advisor, Advise, Pointcut 이라는 개념 덕분에 어떤 부가 기능어디에 적용할 지 명확하게 이해할 수 있었다.

profile
현시깁니다

0개의 댓글