스프링 AOP : 스프링이 제공하는 빈 후처리기, AutoProxyCreator

    이 글은 인프런의 김영한님의 강의를 듣고 복습하며 작성한 글입니다.

     


    스프링이 제공하는 빈 후처리기, AutoProxyCreator

    이전 게시글(https://ojt90902.tistory.com/701) 에서 빈 후처리기를 이용해서 스프링 빈이 등록되는 과정에서 원하는 빈을 선택해서 자동으로 프록시 빈을 생성하고 바꿔치기해서 등록하는 과정을 확인했다. 그런데 이 과정마저 귀찮아서 나온 것이 있다. 바로 스프링 AOP가 제공하는 AutoProxyCreator다. 

     


    AutoProxyCreator

    AutoProxyCreator의 풀 네임은 AnnotationAwareAspectJAutoProxyCreator다. 이 녀석은 스프링 빈 후처리기인데, 특정 빈들만 선택적으로 어드바이스를 넣은 프록시 객체를 만들고 바꿔치기해서 등록을 해주는 역할을 한다. 그렇다면 이 녀석은 어떤 것을 기준으로 특정 스프링빈을 선택해서 어드바이저가 적용된 프록시 객체로 바꿔줄까? 

     

    바로 어드바이저에 저장되어있는 포인트컷을 대상으로 적용 여부를 판단한다. 포인트컷은 어디에 적용할지를 판단하는 필터링 역할을 한다. 이미 어떤 클래스에 어드바이스가 적용되어야 할지 포인트컷은 이미 알고 있다. AutoProxyCrator는 이 사실을 적극 활용해서 스프링 빈을 등록할 때, 자동으로 어드바이스 기능을 가진 프록시 객체를 생성해 스프링 빈으로 등록한다. 

     

    • 스프링 빈으로 등록된 어드바이저를 기반으로 판단해서 어드바이스가 있는 프록시 객체를 생성해서 등록해준다. 

    스프링 AOP가 제공하는 AutoProxyCreator가 하는 일을 정리하면 우선 다음과 같다.

     


    AutoProxyCreator(빈 후처리기임) 세부 기능

    앞서 이야기 했을 때, AutoProxyCreator는 어드바이저를 기준으로 어드바이스가 적용될 스프링 빈을 판단해서 프록시 객체를 만들어 바꿔치기 후 등록해준다고 했다. 좀 더 세부 기능을 살펴보면 다음과 같다.

    1. AutoProxyCreator는 스프링빈이 들어왔을 때, @AspectJ 어노테이션이 있으면 정보를 바탕으로 어드바이저를 만들어 @Aspect 어드바이저 빌더에 저장해준다. 
    2.  AutoProxyCreator는 스프링 빈이 들어왔을 때, 스프링 빈 저장소와 @Aspect 어드바이저 빌드에서 각각 Advisor를 찾아서, 포인트 컷으로 어드바이스 체크 대상인지 확인한다. 어드바이스 체크 대상이면, 프록시 객체를 만들고 그 객체를 스프링 빈으로 대신 등록해준다.

    그림으로 살펴보면 위와 같다. AutoProxyCreator 빈 후처리기의 작동 과정을 살펴보면 다음과 같다.

    1. 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성한다. 
    2. 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
    3. Advisor 조회
      1. Advisor 빈 조회 : 스프링 컨테이너에서 Advisor 빈을 모두 조회한다.
      2. @Aspect Advisor 조회 : @Aspect 어드바이저 빌더 내부에 저장된 Advisor를 모두 조회한다.
    4. 프록시 적용 대상 체크 
    5. 앞서 조회한 Advisor들의 포인트컷을 모두 체크한다. 이 때 객체의 클래스 정보, 객체의 모든 메서드를 포인트컷에 하나씩 매칭해본다. 하나라도 매칭되는 조건이 있으면, 프록시 적용 대상이 된다.
    6. 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하고 프록시를 반환한다. 그리고 프록시를 스프링 빈으로 등록한다. 적용 대상이 아니면 원본 객체를 스프링 빈으로 등록한다.
    7. 빈 등록 : 반환된 객체는 스프링 빈으로 등록한다. 

     


    프록시 자동 생성시 상황별 정리

    어드바이저가 여러 개 있다고 했을 때, 프록시는 어떤 어드바이저를 가지게 될까? 상황별로 정리하면 다음과 같다.

    • advisor1 의 포인트컷만 만족 : 프록시1개 생성, 프록시에 advisor1 만 포함
    • advisor1 , advisor2 의 포인트컷을 모두 만족 :  프록시1개 생성, 프록시에 advisor1 , advisor2 모두 포함
    • advisor1 , advisor2 의 포인트컷을 모두 만족하지 않음 : 프록시가 생성되지 않음

     


    포인트컷은 2가지에 사용된다.

    1. 프록시 적용 여부 판단 - 생성 단계

    • 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지를 확인함.
    • 포인트컷은 클래스 + 메서드 모두 비교함. 이 때, 하나라도 맞으면 프록시를 생성한다. 하나도 없으면 프록시를 생성하지 않음.

    2. 어드바이스 적용 여부 판단 - 사용 단계

    • 프록시가 호출되었을 때, 부가기능인 어드바이스를 적용할지를 포인트컷을 보고 결정함. 
    • 포인트 컷을 만족하면 사용 단계에서 해당 메서드 실행하면서 부가기능을 적용한다. 만족하지 않으면, 실제 타겟 기능만 수행한다. 

     


    AutoProxyCreator 사용 셋팅

    // AOP 추가
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    • AutoProxyCreator는 스프링 AOP가 제공해주는 빈 후처리기다. 따라서 AOP에 대한 의존관계 설정이 되어야한다. 따라서 위 코드를 Build.gradle에 넣어줘야한다.
    • 위 설정을 넣어주면 AnnotationAwareAspectJAutoProxyCreator가 자동으로 스프링 빈으로 등록된다. 

     


    AutoProxyCreator 사용하기 → 어드바이저 빈 등록

    AutoProxyCreator는 어드바이저를 스프링 빈으로 등록하면, 어드바이저의 포인트 컷을 모두 조회해서 포인트 컷의 조건을 만족하는 스프링 빈을 프록시 객체로 바꿔치기 해준다고 했다. 따라서 AutoProxyCreator를 사용하고자 한다면, 어드바이저를 만들어 스프링 빈으로 등록해주면 된다. 

     

    어드바이저 빈 등록 - 어드바이저 한 개만 등록

    @Configuration
    @Import({AppV1Config.class, AppV2Config.class})
    public class AutoProxyConfig {
        @Bean
        public Advisor advisor1(LogTrace logTrace) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* hello.proxy.app..*(..))");
            LogTraceAdvice advice = new LogTraceAdvice(logTrace);
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
            return advisor;
        }
    }
    • AspectJ 포인트컷으로 대상을 지정했다.
    • hello.proxy.app 패키지 아래에 있는 모든 클래스와 메서드에 적용이 되도록 포인트컷 설정함. 
    • 해당 포인트컷을 만족하는 모든 스프링 빈에 대해서 프록시 객체가 만들어 등록된다.
    • 나머지 설정은 할 필요가 없다.(AutoProxyCreator 사용)

    실행해보면 다음과 같이 정상적으로 어드바이스가 먹히는 것을 확인할 수 있다. 

     

    동작 정리

    1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
    2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
    3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor 를 조회한다.
    4. 프록시 적용 대상 체크: 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
    5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
    6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다

    또한 실제 생성된 프록시는 다음과 같은 모습을 가진다. 프록시는 내부적으로 포인트컷에 해당되는 어드바이저를 내부적으로 가지고 있게 된다. 

     

     

    어드바이저 빈 등록 - 어드바이저 2개 등록

    어드바이저를 2개 등록했을 때, 두 개의 어드바이저에 한 스프링 빈이 모두 해당된다고 하자. 이럴 때, 프록시 객체는 하나만 생성되어서 스프링 빈에 등록된다. 대신 이 프록시 객체는 내부적으로 어드바이저를 2개 가지고 있게 된다. 

    @Slf4j
    @Configuration
    @Import({AppV1Config.class, AppV2Config.class})
    public class AutoProxyConfig {
        @Bean
        public Advisor advisor1(LogTrace logTrace) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* hello.proxy.app..*(..))");
            LogTraceAdvice advice = new LogTraceAdvice(logTrace);
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
            return advisor;
        }
    
        @Bean
        public Advisor advisor2(LogTrace logTrace) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* hello.proxy.app..*(..))");
            LogTraceAdvice advice = new LogTraceAdvice(logTrace);
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
            return advisor;
        }
    
    }
    • 위처럼 동일한 어드바이저를 2개 등록할 수 있다. 
    • 동일한 어드바이저이기 때문에 포인트컷에 해당되기만 한다면, 특정 스프링 빈은 2개의 어드바이저를 가지게 된다. 
    • 따라서 프록시 객체는 하나 생성되고, 그 프록시 객체는 동일하지만 이름이 다른 어드바이저 2개를 가지고 스프링 빈에 등록된다.

    실행 결과는 다음과 같다. 현재 로그 추적기는 쓰레드 로컬로 만들어졌다. 따라서, 동일한 쓰레드는 동일한 TraceId를 공유하기 때문에 두 번 적용되었기 때문에 실행 레벨이 2배로 올라간다고 보면 된다. 즉, 정상적으로 출력된 것이다. 

    한 객체에 여러 어드바이저가 해당된다고 하더라도, 실제로 생성되는 프록시 객체는 하나다. 그리고 그 프록시 객체는 내부적으로 여러 어드바이저를 가진다. 

     


    AutoProxyCreator 사용하기 → @Aspect 스프링 빈 등록하기

    AutoProxyCreator는 들어온 스프링 빈 중에 @Aspect 어노테이션이 있는지 확인한다. 만약에 @Aspect 어노테이션이 있다면 해당 클래스의 @Around 같은 정보 + @Pointcut 정보 등을 읽어서 어드바이저로 만들어 @Aspect 어드바이저 빌더 저장소에 저장을 해둔다. 그리고 AutoProxy는 스프링 빈 저장소에 있는 어드바이저를 검색하는 것과 동일하게 어드바이저 빌더에 있는 어드바이저를 찾아서 동일하게 처리해준다.

     

    @Aspect 클래스 구현 → 어드바이저 클래스를 구현해준다.

    @Aspect
    public class LogTraceAspect {
    
        private final LogTrace logTrace;
    
        public LogTraceAspect(LogTrace logTrace) {
            this.logTrace = logTrace;
        }
    
        @Around("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..*noLog*(..))")
        public Object logTraceGo(ProceedingJoinPoint joinPoint) throws Throwable {
            TraceStatus status = null;
    
            try{
                String messsage = joinPoint.getSignature().toShortString();
                status = logTrace.begin(messsage);
    
                Object result = joinPoint.proceed();
                logTrace.end(status);
                return result;
            }catch (Exception e){
                logTrace.exception(status, e);
                throw e;
            }
        }
    }
    • @Around 아래에 있는 메서드는 어드바이스로 후에 변환된다. 
    • 따라서 클래스 내부에 로그를 찍는 어드바이스를 동일하게 작성한다.
    • 이 때 joinPoint.proceed()를 이용해 실제 클래스를 호출할 수 있다.
    • execution으로 되어있는 부분은 AspectJExpressionPoinCut처럼 동작한다. 

     

    @Aspect 클래스 스프링 빈 등록 → AutoProxyCreator는 @Aspect 스프링 빈을 어드바이저로 변경해준다.

    @Import({AppV1Config.class, AppV2Config.class})
    @Configuration
    public class AopConfig {
    
        @Bean
        public LogTraceAspect logTraceAspect(LogTrace logTrace) {
            return new LogTraceAspect(logTrace);
        }
    
    }
    • 다음과 같이 @Aspect가 붙은 클래스를 생성해서 스프링 빈으로 등록해주면 끝이다.

     

     

    @Aspect를 어드바이저로 변환해서 저장하는 과정

    1. 실행: 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.
    2. 모든 @Aspect 빈 조회: 자동 프록시 생성기는 스프링 컨테이너에서 @Aspect 애노테이션이 붙은 스프링 빈을 모두 조회한다.
    3. 어드바이저 생성: @Aspect 어드바이저 빌더를 통해 @Aspect 애노테이션 정보를 기반으로 어드바이저를 생성한다.
    4. @Aspect 기반 어드바이저 저장: 생성한 어드바이저를 @Aspect 어드바이저 빌더 내부에 저장한다

     

     @Aspect 어드바이저 빌더는 @Aspect 의 정보를 기반으로 어드바이저를 만들고, @Aspect 어드바이저 빌더 내부 저장소에 캐시한다. 캐시에 어드바이저가 이미 만들어져 있는 경우 캐시에 저장된 어드바이저를 반환한다

     


    정리 

    기존에는 빈 후처리기를 개발자가 직접 만들어서 스프링 빈으로 등록해야했다. 사실 큰 일은 아니지만, 시간이 낭비되는 부분은 맞다. 그런데 스프링은 스프링 AOP를 통해서 AutoProxyCreator(AnnotationAwareAspectJAutoProxyCreator)를 통해서 스프링 빈으로 등록된 어드바이저나 @Aspect 어노테이션을 구별해서 어드바이저를 가진 프록시 객체를 생성해준다.

    즉, 기존보다 더욱 더 손쉽게 횡단 관심사를 처리할 수 있게 해주었다. 

    실무에서는 위처럼 횡단 관심사를 처리할 때, 스프링 AOP를 주로 사용한다고 한다. 특히 AspectJ 포인트컷이라는 강력한 툴을 제공하면서 세심하고 정밀하게 원하는 곳에 횡단 관심사를 등록할 수 있게 된다.

    댓글

    Designed by JB FACTORY