[AOP] 로그 기능 적용 : 빈 후처리기

Heechul Yoon·2022년 6월 12일
0
post-custom-banner

동적프록시 적용 포스팅에서 config를 따로 만들어서 타겟에 동적프록시가 적용되도록하고 연관된 빈을 일일이 연결해줬다. 이렇게 동적 프록시를 적용하면 컨테이너의 컴포넌트 스캔 대상에 들어갈 수 없기 때문에 불편한 점이 있었다. 컴포넌트 스캔을 도중 빈이 컨테이너에 등록 되기 전에 훅을 걸어서 프록시 의존성을 주입해주면 이런 문제를 해결 할 수 있다.

빈 후처리기

스프링 컨테이너를 등록할 때 스프링은 컴포넌트들을 바로 컨테이너에 등록하는게 아니라 빈 후처리기에 넣어서 후처리기로 부터 빈을 받고나서 등록하게 된다.
후처리기 안에서는 전달받은 빈 객체를 조작하거나 혹은 새로운 빈 객체로 바꿔서 리턴하게 되며, 스프링은 후처리기에게 받은 빈을 컨테이너에 등록한다.

예를들어 빈 후처리기에 postingController를 집어넣으면 빈 후처리기는 postingController 객체를 타겟으로 하는 proxy 객체를 리턴하게 된다. 그리고 스프링 컨테이너에는 proxy가 컨트롤러로서 등록이 된다

BeanPostProcessor

후처리기를 우선 먼저 스프링컨테이너에 등록해야한다. 우선 BeanPostProcessor 인터페이스를 구현하는 클래스를 만들어주어야 한다

public class PackageLogTraceProxyPostProcessor implements BeanPostProcessor {
    private final String basePackage;
    private final Advisor advisor;
    public PackageLogTraceProxyPostProcessor(String basePackage, Advisor advisor) {
        this.basePackage = basePackage;
        this.advisor = advisor;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //프록시 적용 대상 여부 체크
        //프록시 적용 대상이 아니면 원본을 그대로 반환
        String packageName = bean.getClass().getPackageName();
        if (!packageName.startsWith(basePackage)) {
            return bean;
        }
        
        //프록시 대상이면 프록시를 만들어서 반환
        ProxyFactory proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(advisor);
        Object proxy = proxyFactory.getProxy();
        
        return proxy;
    }
}

BeanPostProcessor 인터페이스에는 빈을 초기화 하고 난 이후에 어떤 처리를 해주는 postProcessAfterInitialization와 빈을 초기화 하기 전에 어떤 처리를 해주는 postProcessBeforeInitialization 가 있다. 지금은 컨트롤러를 프록시로 바꿔주는거기 때문에 postProcessAfterInitialization를 사용한다.

이제 BeanPostProcessor를 상속받는 후처리기를 만들어주자

public class LogTracePostProcessor implements BeanPostProcessor {
    private final String basePackage;
    private final Advisor advisor;

    public LogTracePostProcessor(Advisor advisor) {
        this.basePackage = "community.app.controller";
        this.advisor = advisor;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        String packageName = bean.getClass().getPackageName();

        if (!packageName.startsWith(basePackage)) { // [1]
            return bean;
        }

        ProxyFactory proxyFactory = new ProxyFactory(bean); // [2]
        proxyFactory.addAdvisor(advisor);
        Object proxy = proxyFactory.getProxy();

        return proxy;
    }
}

이제 이 LogTracePostProcessor는 Config에서 스프링 컨테이너에 등록되어 가장 먼저 컨테이너 안에 모든 빈들을 대상으로 적용 될 것이다.
이 뜻은 스프링 내부적으로 사용하는 빈들도 전부 LogTracePostProcessor를 거치게 된다.
그래서 [1]에서 처럼 프록시가 적용되길 원하는 패키지가 아니라면 원래 빈을 반환하도록 한다.(아니면 컨테이너 안에 있는 모든 빈에 프록시가 적용된다ㅎㅎ..)
그리고 컨테이너 안의 수많은 빈 중에서 내가 지정한 빈이 넘어오면 [2]에서 프록시를 생성하고 Advisor를 넣어주고, 넘어온 빈을 생성하는게 아닌, bean을 타겟으로 하는 프록시를 컨테이너에 넘겨준다.

Config

빈 후처리클래스를 만들어 줬다면 이걸 Config를 통해서 스프링 컨테이너에 등록해야 스프링 컨테이너가 "후처리기가 등록되었으니 이걸 먼저 적용해야 겠군" 이러고 적용된다.

@Configuration
public class ProxyConfig {

    @Bean
    public LogTracePostProcessor logTracePostProcessor() {
        return new LogTracePostProcessor(getAdvisor(new LogTracer()));
    }

    private Advisor getAdvisor(LogTracer logTracer) {
        LogTraceAdvice advice = new LogTraceAdvice(logTracer);
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("get*");

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

포인트컷은 모든 메서드에 적용할 예정이라 Pointcut.TRUE를 넣어주도록 한다.

스프링에서 제공하는 빈 후처리기

스프링에서는 위에서 해준 모든 작업들을 해주는 빈 후처리기를 제공한다.

implementation 'org.springframework.boot:spring-boot-starter-aop'

우선은 build.gradle파일에 위의 라이브러리를 추가한다.

이 라이브러리를 추가해서 다운받으면 aspectjweaver라는 aspectj 라이브러리가 들어오는데, 여기서 스프링 부트가 AOP관련 빈들을 자동으로 컨테이너에 등록해준다.
이 AOP관련 빈들 중에서 AnnotationAwareAspectJAutoProxyCreator를 등록해주는데, 이게 빈 후처리기 BeanPostProcessor 인터페이스를 구현한 구현체이다이다.
AnnotationAwareAspectJAutoProxyCreator는 Auto가 들어간 만큼 정말 많은것들을 자동으로 해준다. 우선 스프링 컨테이너에 등록될 모든 빈을 조회하고, MethodInterceptor를 상속받은 모든 Advisor안에 있는 Pointcut을 확인한다.
즉, Pointcut안에 있는 메서드가 Bean안에 있는 메서드가 하나라도 일치한다면 프록시를 만든다.
예를 들어서 어떤 빈에 10개의 매서드중 하나만 Pointcut에 있는 메서드를 만족한다면 프록시 적용의 대상이 된다. 그래서 빈을 타겟으로 들고 있는 프록시 객체가 컨테이너에 등록된다.

정밀한 프록시 메서드 타겟 설정

위에서 pointcut.setMappedNames("get*")를 통해서 빈에 get이 들어가는 메서드가 하나라도 있으면 프록시가 적용된다. 하지만 다음과 같은 문제가 발생한다

사진을 보면 스프링 내부적으로 어플리캐이션을 돌리기 위해서 실행하는 Tomcat같은 빈안에서 get이 들어가는 매서드가 있다면 전부 프록시를 적용해서 실행해버려서 위와같은 로그가 찍힌걸 볼 수 있다.
이런 문제를 해결하기 위해 패키지까지 걸러주는 정밀한 포인트컷이 필요하다.

@Configuration
public class ProxyConfig {

    @Bean
    public Advisor advisor1() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* community.app.controller..*(..))");

        LogTraceAdvice advice = new LogTraceAdvice(new LogTracer());

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

AspectJ에서 제공하는 좀 더 정밀한 포인트 컷을 사용해서 패키지와 메서드 시그니처를 지정해서 포인트컷을 적용할 수 있다. "execution(* community.app.controller..*(..))" 이거는 community.app.controller 패키지의 하위 패키지 안에 있는 모든 메서드 시그니처에 해당하는 메서드를 포인트컷의 적용대상으로 등록해준 것이다. 만약 community.app.controller 패키지 안에 있는 클래스들 중에서 get이 들어가는 메서드에만 포인트컷을 적용해주고 싶다면 "execution(* community.app.controller..get*(..))" 이렇게 적어주면 된다.

여러개의 Advisor 적용

만약 여러개의 Advisor가 config에서 등록되고, 한 프록시에서 모든 Advisor의 Pointcut을 만족한다면 프록시에는 두개의 Advisor가 등록된다

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

명심해야 될것은 프록시 하나당 Advisor하나가 아니라 하나의 프록시는 여러개의 Advisor를 가질 수 있고, 각각의 Advisor이 연쇄적으로 호출되어 마지막에 타겟 객체가 호출된다.

profile
Quit talking, Begin doing
post-custom-banner

0개의 댓글