스프링 AOP : Bean PostProcessor를 이용한 의존관계 주입

    이 포스팅은 인프런의 김영한님 강의를 복습하며 정리한 글입니다.

     


    프록시 팩토리의 한계

    앞선 글(https://ojt90902.tistory.com/701)에서 프록시 팩토리만으로는 @Component로 등록되는 스프링 빈들을 프록시 형태로 처리 해줄 수 없다는 한계점이 있다는 것을 알았다. 덤으로 의존관계 주입도 하나하나 @Configuration에서 해야하는데, 이 설정도 무척이나 번거롭다.

    그렇다면 프록시 팩토리만 사용했을 때의 위 한계점은 어떻게 극복할 수 있을까?

     


    빈 후처리기(Bean PostProcessor)의 활용

    기본적으로 스프링 빈은 위의 그림처럼 등록이 된다. @Bean, @Component, @Import 어노테이션이 붙은 클래스들을 대상으로 스프링은 빈을 생성해서 스프링 빈 저장소에 자동으로 등록을 해준다. 이후에는 스프링의 빈 저장소에서 빈을 검색해서 의존관계 주입 등으로 사용한다. 

     

    스프링 빈 후처리기(Bean PostProcessor)는 스프링이 객체를 생성해서 스프링 빈 저장소에 저장하기 직전에 작용을 하는 녀석이다. 실제 동작과정은 다음과 같이 동작한다.

    1. 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성한다(@Bean, @Import, @Component 등)
    2. 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달해준다.
    3. 후 처리 작업 : 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기하거나, 그대로 전달한다
    4. 등록 : 빈 후처리기는 빈을 반환한다. 반환된 빈은 스프링 빈 저장소에 저장된다. 

    실제 코드 동작 결과에서도 알 수 있다. BeanPostProcessorChecker가 BeanPostProcessor가 있는지 확인하고, BeanPostProcessor가 있으면 빈 후처리기로 생성된 빈을 넘겨주는 것을 볼 수 있다. 

     


    빈 후처리기(Bean PostProcessor) 사용 방법

    빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현해서 스프링 빈으로 등록을 해주면 된다. 인터페이스를 구현하면 아래 두 가지 메서드 중 하나를 선택해서 구현하면 된다.

    • postProcessBeforeInitialization : 빈 후처리기가 객체 생성 이후 @PostConsturct 같은 초기화가 발생하기 전에 호출됨.
    • postProcessAfterInitiatiozation : 빈 후처리기가 객체 생성 이후 @PostConsturct 같은 초기화가 발생 후에 호출됨

     

    빈 후처리기가 스프링 빈으로 등록되면, 스프링은 모든 빈을 등록하기 직전에 모든 빈 후처리기를 한번씩 다 통과하게 해준다. 이것이 의미하는 것은 모든 빈이 통과하기 때문에 모든 빈을 변경할 것이 아니라면, 적절한 필터링이 필요하다는 것이다. 

     

    참고 : @PostConsturct의 비밀

    @PostConstruct는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화는 단순히 @PostConstruct 에노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다. 더 쉽게 이야기 하면 생성된 빈을 한번 조작한다. 그렇다면 @PostConstruct는 어떻게 빈을 조작할까?

    스프링은 CommonAnnotationBeanPostPrcessor라는 빈 후처리기를 자동으로 등록하는데, 여기에서 @PostConstruct 어노테이션이 붙은 메서드를 호출한다. 

     

     

    빈 후처리기 테스트 코드로 간단히 알아보기 

    빈 후처리기 사용하지 않을 때

    public class BasicTest {
    
        @Test
        void noPostProcessor() {
    
            AnnotationConfigApplicationContext context = new
                    AnnotationConfigApplicationContext(BasicConfig.class);
    
            classA a = context.getBean("A", classA.class);
            a.call();
        }
    
        @Configuration
        static class BasicConfig{
    
            @Bean
            public classA A() {
                return new classA();
            }
    
        }
    
        static class classA{
            public void call(){
                log.info("callA");
            }
        }
    
        static class classB{
            public void call(){
                log.info("callB");
            }
        }
    }
    • 스프링 빈으로 classA()를 만들어서 빈 저장소에 등록했다.
    • 테스트 코드 내에서는 스프링 빈 저장소에서 빈 이름으로 값을 찾아와서, call() 함수를 실행했다. 
    • 실행 결과는 classA의 call() 결과인 "callA"가 나온다. 

     

    빈 후처리기를 사용할 때

    빈 후처리기를 사용하기 위해서는 BeanPostProcessor 인터페이스를 구현하고, 빈으로 등록해주면 된다. 

    public class BeanPostPrcoessorTest {
    
    
        @Test
        void beanPostProcessorTest() {
    
            AnnotationConfigApplicationContext context = new
                    AnnotationConfigApplicationContext(BasicConfig.class);
    
            classB a = (classB) context.getBean("A");
            a.call();
        }
    
        @Configuration
        static class BasicConfig{
            @Bean
            public classA A() {
                return new classA();
            }
            @Bean
            public Postprocessor postprocessor() {
                return new Postprocessor();
            }
        }
    
        static class Postprocessor implements BeanPostProcessor {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                return new classB();
            }
        }
    
        static class classA{
            public void call(){
                log.info("callA");
            }
        }
    
    
        static class classB{
            public void call(){
                log.info("callB");
            }
        }
    
    }
    
    • 빈 포스트 프로세서를 구현했고, 빈 생성 + 초기화 이후에 등록된 빈들이 classB로 바꾸어 등록되도록 코드를 작성했다.
      1. @Bean으로 등록하는 것은 classA()다.
      2. classA()는 생성되어서 등록되는 과정에 빈 포스트 프로세서로 들어간다.
      3. 빈 포스트 프로세서는 classB()를 반환해준다.
      4. 따라서 A라는 스프링 빈에 저장된 객체는 classB()고 실제 call() 메서드 호출 결과는 "callB"가 나오게 된다. 

    실제로 동작되는 것은 다음과 같이 된 것으로 이해할 수 있다. 

     


    실제 코드에 적용하기

    실제 코드에 적용하기 위해서는 위에서 했던 것과 동일한 일을 해야한다. BeanPostProcessor의 구현체 구현한다. 그리고 구현체를 스프링 빈으로 등록해준다. 스프링 빈으로 등록해주면, 스프링 부트가 뜰 때 BeanPostProcessor 체커가 확인 후, 해당 빈 포스트 프로세서에 빈을 전달해준다.

     

    Bean PostProcessor 구현 

    @Slf4j
    public class PackageLogTracePostProcessor implements BeanPostProcessor {
    
        private final String basePackageName;
        private final Advisor advisor;
    
        public PackageLogTracePostProcessor(String basePackageName, Advisor advisor) {
            this.basePackageName = basePackageName;
            this.advisor = advisor;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            String baseName = bean.getClass().getPackage().getName();
            
            // 적용하지 않은 패키지 필터링
            if (!baseName.startsWith(basePackageName)) {
                return bean;
            }
    
            // 모두 동적 프록시로 만들어짐. 
            // 포인트컷으로 특정 메서드만 적용 
            ProxyFactory proxyFactory = new ProxyFactory(bean);
            proxyFactory.addAdvisor(advisor);
            Object proxy = proxyFactory.getProxy();
    
            log.info("프록시가 만들어졌습니다. beanName = {}, 프록시 = {}", beanName, proxy.getClass());
            return bean;
        }
    }
    • 모든 빈은 등록될 때, 빈 후처리기를 통과한다.
    • 모든 빈에 어드바이저를 등록할 필요는 없다. 따라서 등록하고 싶지 않으면 return bean 처리를 해준다.
    • 등록이 필요한 빈은 프록시 객체를 만들어 프록시 객체로 바꿔치기 해준다. 

    이 때 구분해서 알아둬야 할 부분은 빈 포스트 프로세서는 빈을 바꿔치기 해주는 용도다. 포인트컷은 등록된 빈에서 어떤 메서드가 실행될 때만 어드바이스를 적용 할 지를 결정한다. 

    • 빈 포스트 프로세서 : 빈 객체를 바꿔치기 해줌
    • 포인트컷 : 프록시 빈 객체에 등록된 필터링 기능임. 프록시 빈 객체가 소환되고 동적으로 메서드가 넘어왔을 때, 이 메서드가 이 클래스에서 실행되어도 될지 안될지를 결정해줌. 

    위의 개념을 명확히 구별해야한다. 

     

    Bean PostProcessor의 스프링 빈 등록

    @Import({AppV1Config.class, AppV2Config.class})
    @Configuration
    public class BeanPostProcessorConfig {
    
        @Bean
        public PackageLogTracePostProcessor packageLogTracePostProcessor(LogTrace logTrace) {
            Advisor advisor = getAdvisor(logTrace);
            return new PackageLogTracePostProcessor("hello.proxy.app", advisor);
        }
    
        private Advisor getAdvisor(LogTrace logTrace) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* hello.proxy.app..*(..))");
            LogTraceAdvice advice = new LogTraceAdvice(logTrace);
            return new DefaultPointcutAdvisor(pointcut, advice);
        }
    }
    • @Configuration을 통해 빈 후처리기를 등록할 수 있다.
    • 빈 후처리기는 프록시 객체로 바꿔주고, 프록시 객체에 어드바이저를 이용해서 넘겨주기 때문에 어드바이저를 넘겨줬다. 
    • 프록시 객체에 등록된 포인트 컷은 모든 메서드에 동작할 수 있도록 AspectJExpressionPointCut을 사용했다. 

     

    실행 결과

    스프링 부트를 띄워보면, 다음과 같이 빈 포스트 프로세서를 통과하면서 원하는 빈 객체들이 프록시 객체로 변경되어 스프링 빈 저장소에 저장되는 것을 알 수 있다. 그렇다면 어떻게 각 스프링 빈들의 프록시 객체 의존관계가 형성되는 것일까?

     

    스프링 빈 의존관계 설정 

    각 객체들의 의존관계가 설정되는 것은 위의 @Configuartion 덕분이다.

    예를 들어 orderRepositoryV1이라는 이름의 스프링빈이 스프링 빈 저장소에 저장된다고 해보자. 이 때, 초기에 스프링 빈 객체로 생성되는 것은 OrderRepositoryV1Impl 타입의 인스턴스다. 그런데 이 객체는 스프링 빈 저장소에 저장되기 직전에 빈 포스트 프로세서를 통과하면서 내부적으로 OrderRepositoryV1Impl을 가지는 프록시 객체가 스프링 빈으로 등록된다.

    orderServiceV1은 내부적으로 orderRepositoryV1()의 결과로 받는 빈의 의존관계를 주입 받는다. 따라서 orderServiceV1Impl은 orderRepositoryV1()의 결과인 빈 프록시 객체를 내부 참조한다. 그리고 이 객체는 스프링 빈 저장소에 저장되기 전에 빈 후처리기에 들어가면서 다시 한번 빈 프록시 객체로 변경된다.

     

    @ComponentScan의 대상도 유사하게 동작한다. OrderRepositoryV3가 빈 저장소에 등록될 때, 빈 후처리기를 통해 orderRepositoryV3이라는 빈 이름으로 OrderRepositoryV3의 인스턴스 참조를 가지는 프록시 객체가 저장된다. 

    이후 OrderServiceV3가 빈 저장소에 등록될 때, @AutoWired를 통해 OrderRepositoryV3의 이름을 가지는 스프링 빈 저장소에서 값을 주입받는다. 이 때, 이름으로 검색을 하게 되는데 OrderRepositoryV3 이름을 가지는 빈은 실제로는 프록시 객체이기 때문에 프록시 객체를 참조로 가지게 된다. 

     


    빈 후처리기 정리

    프록시 팩토리만 사용했을 때의 문제점은 2개였다.

    1. 너무 많은 의존관계 설정을 개발자가 직접 해야한다. 
    2. 컴포넌트 스캔의 대상은 프록시 팩토리를 직접적으로 이용할 수 없었다.

    1번은 그렇다 치더라도, 2번은 정말 방법이 없었다. 왜냐하면 이미 @Component에 의해서 스프링 빈으로 자동으로 등록된 스프링 빈을 프록시 팩토리를 통해서 등록할 수 있는 방법이 없었기 때문이다. 그런데 이런 문제는 빈 후처리기를 이용하면서 처리가 가능했다.

     

    스프링은 빈을 등록할 때, Bean PostProcessor를 통해서 빈 후처리기가 스프링 빈 저장소에 있는지 확인하고, 있을 경우 모든 스프링 빈이 등록되기 전에 Bean PostProcessor를 통과하도록 설정한다. 따라서, @Component, @Bean, @Import 등의 스프링 빈이 등록될 때 반드시 이 빈 후처리기를 통과하기 때문에 프록시 객체 생성이 필요한 것들을 Bean PostProcessor에서 적절하게 변경될 수 있도록 해주면 되었다. 

     

    그런데 개발자의 욕심은 끝이 없다. 이 빈 후처리기를 생성하는 로직마저도 비슷비슷한 거 같으니, 누가 해줬으면 좋겠다는 마음을 가지게 된다. 그 마음은 스프링이 들어준다. 스프링 AOP는 빈 후처리기마저 자동으로 제공해준다. 다음 포스팅에서는 스프링이 제공해주는 빈 후처리기에 대해 알아보고자 한다.

    댓글

    Designed by JB FACTORY