[Spring] Advice, PointCut, Advisor 란?

반응형

0. 들어가기전에, 프록시 팩토리 (Proxy Factory) 정리

스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리 (ProxyFactory) 기능을 제공한다. 프록시 팩토리 하나로 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 인터페이스 없이 구체클래스만 존재하면 CGLIB 를 사용할수 있다. 

이때, InvocationHandler 와 MethodInterceptor 를 따로 만들어야 했던 문제를 해결하기위해 스프링은 Advice 라는 개념을 도입했다. 각 핸들러와 인터셉터를 신경쓰지 않고, Advice 만 만들면 된다.

 

 

1. Advice, PointCut, Advisor 란?

 

(1) Advice (어드바이스)

JDK 동적 프록시의 InvocationHandler 와 CGLIB 의 MethodInterceptor 를 대신하여 부가기능을 적용할 때 Advice 라는 개념을 도입했다. Advice 만 작성한다. 프록시 팩토리를 사용하면 내부적으로 각각 InvocationHandler, MethodInterceptor 가 Advice를 호출한다.

프록시가 호출하는 부가 기능 (프록시 로직)

org.aopalliance.intercept.MethodInterceptor

 

(2) PointCut (포인트컷)

특정 조건에 해당할때만 프록시 부가기능이 적용되도록 하는 경우, 스프링은 Pointcut 이라는 개념을 도입해서 해결한다.

어디에 부가 기능을 적용할 것인가를 판단하는 필터링 로직

주로 클래스와 메소드명으로 필터링 한다

어떤 포인트 (Point) 에 기능을 적용하지 않을지 자르는 (Cut) 것

 

(3) Advisor (어드바이저)

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

Pointcut 1 + Advice 1

 

 

이렇게 구분하는 것은 역할과 책임을 명확하게 분리한 것이다. 어드바이스부가기능 로직만 담당하고, 포인트컷은 대상인지 확인하는 필터 역할만 담당한다. 이 둘을 합친 것이 어드바이저이다.

 

 

2. 예제코드

(1) 인터페이스와 그 구현 클래스 : CommandInterface, CommandImpl

// 인터페이스
public interface CommandInterface {
    String execute1();
    String execute2();
}
// 구현
@Slf4j
public class CommandImpl implements CommandInterface {

    @Override
    public String execute1() {
        log.info("execute1() 호출");
        return "execute1() complete";
    }

    @Override
    public String execute2() {
        log.info("execute2() 호출");
        return "execute2() complete";
    }

}

(2) 실행시간 출력하는 부가로직 : TimeCheckAdvice

패키지명 유의 : org.aopalliance.intercept

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

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

        // target 클래스 호출하고 그 결과를 취득
        Object result = invocation.proceed(); // 실제 로직

        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;
        log.info("TimeCheckAdvice 종료 resultTime = {}", resultTime);

        return result;
    }
}

(3) 실행 (테스트 코드)

public class AdvisorBasicTest {

    @Test
    void springPointcutBasic() {
        CommandInterface target = new CommandImpl();

        ProxyFactory proxyFactory = new ProxyFactory(target);
        // 스프링이 제공하는 NameMatchMethodPointcut : 메소드명 기반, 내부에서 PatternMatchUtils 사용
        // 가장 많이 사용하는 것은 aspectJ 표현식 기반인 AspectJExpressionPointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("execute2"); // 적용할 메소드명
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeCheckAdvice());
        proxyFactory.addAdvisor(advisor);

        CommandInterface proxy = (CommandInterface) proxyFactory.getProxy();

        proxy.execute1();
        proxy.execute2();
    }
}

(4) 결과 확인

CommandImpl - execute1() 호출
TimeCheckAdvice - TimeCheckAdvice 실행
CommandImpl - execute2() 호출
TimeCheckAdvice - TimeCheckAdvice 종료 resultTime = 0

execute1() 호출 : 어드바이스 적용 X

execute2() 호출 : 어드바이스 적용 O

 

◾ 여러 어드바이저 적용

아래와 같이 프록시 팩토리에 addAdviosr() 를 통해 원하는 만큼 어드바이저를 등록할 수 있다.

등록하는 순서대로 advisor 가 호출 된다.

CommandInterface target = new CommandImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// advisor2 -> advisor1 순서로 호출된다.
proxyFactory.addAdvisor(advisor2);
proxyFactory.addAdvisor(advisor1);

 

3. 정리

프록시 팩토리를 통해 프록시를 편리하게 생성할 수 있다. 스프링의 어드바이저, 어드바이스, 포인트컷 이라는 개념으로 부가 기능적용여부을 편리하게 정의 할 수 있다.

 

4. 문제

문제는 너무 많은 설정을 해야 한다는 것과 컴포넌트 스캔을 이용하는 경우 프록시 적용이 되지 않는다는 것이다.

 

(1) 많은 설정 요구

예제코드에서는 인터페이스 CommandInterface 와 그 구현 클래스 CommandImpl 가 있을 경우 부가기능을 적용하기위해 ProxyFactory, NameMatchMethodPointcut, DefaultPointcutAdvisor 을 설정해줬다. 클래스가 100개 존재하면 이러한 설정도 100개가 필요해진다.

 

(2) 컴포넌트 스캔의 프록시 적용 문제

컴포넌트 스캔으로 클래스를 스프링빈으로 등록하면 위의 방법으로는 프록시 적용이 되지 않는다. 이미 실제 객체를 컴포넌트 스캔을 통해 스프링 컨테이너에 스프링 빈으로 등록을 해 놓은 상태이기 때문이다. 위의 방법으로 프록시를 적용하기 위해서는 실제 객체를 등록하는 것이 아니라, 부가기능이 있는 프록시를 실제 객체 대신 스프링 빈으로 등록해야 하는 것이다.

위의 문제를 해결하는 방법이 없을까?

 

빈 후처리기 (BeanPostProcessor) 를 이용하자!

 

 

 

반응형