Spring/스프링 기본

포인트 컷, 어드바이스, 어드바이저(PointCut, Advice, Advisor)

공대키메라 2022. 3. 28. 14:09
이 내용은 인프런 김영한 선생님의 스프링 핵심 원리 - 고급편을 바탕으로 정리한 것입니다.

 

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

 

AOP관련 지식을 조금 알면 이에 대해 들어봤을 것이다.

 

오늘은 이에 대한 지식과 함께 어떻게 구현하는지 알아볼 것이다. 

 

1. 용어 정리

 

포인트컷(Pointcut)

어디에 부가 가능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직.

이름 그대로 어떤 포인트에 기능을 적용할지 안할지를 잘라서 구분하는 것이다. 

이름 그대로 각각의 포인트를 cut해서 구분한다는 느낌?

 

어드바이스(Advice)

프록시가 호출하는 부가 가능. 프록시 로직.

 

어드바이저(Advisor)

하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것. 

 

이렇게 구분함으로써 얻을수 있는 장점은 역할과 책임을 명확히 구분할 수 있다는 것이다. 

 

 

2. 구현하기 

 

ServiceInterface.java

package hello.common.service;

public interface ServiceInterface {
    void save();
    void find();
}

 

 

ServiceImpl.java

package hello.common.service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ServiceImpl implements ServiceInterface{
    
    @Override
    public void save() {
        log.info("save 호출");
    }

    @Override
    public void find() {
        log.info("find 호출");
    }

}

 

TimeAdvice.java

package hello.common.advice;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class TimeAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

//        Object result = method.invoke(target, args); // call
        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTim={}", resultTime);
        return result;
    }

}

 

AdvisorTest.java

package hello.advisor;

import hello.common.advice.TimeAdvice;
import hello.common.service.ServiceImpl;
import hello.common.service.ServiceInterface;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

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

}

 

현재 상황에서는 save(), find()메서드 둘 다 적용이되지만 포인트컷을 이용하면 어느 하나를 로직을 사용하지 못하도록 할 수 있다. 

 

우리는 find()메서드를 사용하지 못하게 할 것이다.

 

포인트컷은 크게 ClassFilter와 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();
    
    /*
    결과
    
    13:16:42.150 [main] INFO hello.advisor.AdvisorTest - 포인트컷 호출 method=save targetClass=class hello.common.service.ServiceImpl
    13:16:42.155 [main] INFO hello.advisor.AdvisorTest - 포인트컷 결과 result=true
    13:16:42.160 [main] INFO hello.common.advice.TimeAdvice - TimeProxy 실행
    13:16:42.160 [main] INFO hello.common.service.ServiceImpl - save 호출
    13:16:42.161 [main] INFO hello.common.advice.TimeAdvice - TimeProxy 종료 resultTim=0
    13:16:42.161 [main] INFO hello.advisor.AdvisorTest - 포인트컷 호출 method=find targetClass=class hello.common.service.ServiceImpl
    13:16:42.161 [main] INFO hello.advisor.AdvisorTest - 포인트컷 결과 result=false
    13:16:42.161 [main] INFO hello.common.service.ServiceImpl - find 호출
    */
}

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

 

method가 이름이 save인 경우에만 advisor가 작동하도록 설정했다. 

 

이것은 이미 스프링에서 이러한 기능을 잘 활용할 수 있도록 포인트컷 관련 기능을 제공하고 있다. 

 

 

NameMatchMethodPointCut 사용

@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();
    /*
    결과
    
    13:21:10.554 [main] INFO hello.common.advice.TimeAdvice - TimeProxy 실행
    13:21:10.559 [main] INFO hello.common.service.ServiceImpl - save 호출
    13:21:10.559 [main] INFO hello.common.advice.TimeAdvice - TimeProxy 종료 resultTim=1
    13:21:10.562 [main] INFO hello.common.service.ServiceImpl - find 호출
    */
}

 

스프링에서 제공하는 포인트컷은 굉장히 종류가 많다. 

 

  • NameMatchMethodPointcut
  • JdkRegexpMethodPointcut
  • TruePointcut
  • AnnotationMatchingPointcut
  • AspectJExpressionPointcut 등등

그런데 AspectJExpressionPointcut만 거의 사용한단다.

 

advice 는 여러개도 적용이 가능하다. 

 

MultiAdvisorTest.java

package hello.advisor;

import hello.common.advice.TimeAdvice;
import hello.common.service.ServiceImpl;
import hello.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;

public class MultiAdvisorTest {

    @Test
    @DisplayName("여러 프록시")
    void multiAdvisorTest1(){
        //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();
        
        /*
        결과
        
        13:21:10.554 [main] INFO hello.common.advice.TimeAdvice - TimeProxy 실행
        13:21:10.559 [main] INFO hello.common.service.ServiceImpl - save 호출
        13:21:10.559 [main] INFO hello.common.advice.TimeAdvice - TimeProxy 종료 resultTim=1
        13:21:10.562 [main] INFO hello.common.service.ServiceImpl - find 호출
        
        */
    }

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

 

그런데 이러한 경우 advisor를 여러개 적용할 수 있다는 것을 확인했지만, advisor를 적용할 때 마다 프록시를 계속 생성해야 하는 문제가 있다.

 

스프링에서 이를 해결하기 위해 하나의 프록시에서 여러개의 어드바이저를 적용하는 기능을 제공한다. 

 

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

@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2(){
    //client -> proxy -> advisor2 -> 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();
    
    /*
    결과
    
    13:39:19.556 [main] INFO hello.advisor.MultiAdvisorTest$Advice2 - advice2 호출
    13:39:19.559 [main] INFO hello.advisor.MultiAdvisorTest$Advice1 - advice1 호출
    13:39:19.559 [main] INFO hello.common.service.ServiceImpl - save 호출
    
    */
}