WEB/Spring

스프링 핵심 원리 - 고급편 6) 빈 후처리기

Tony Lim 2022. 6. 9. 16:18
728x90

 일반적인 스프링 빈 등록 과정

public class BasicTest {

    @Test
    void basicConfig() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);

        //A는 빈으로 등록된다.
        A a = applicationContext.getBean("beanA", A.class);
        a.helloA();

        //B는 빈으로 등록되지 않는다.
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.class));
    }

    @Slf4j
    @Configuration
    static class BasicConfig {
        @Bean(name = "beanA")
        public A a() {
            return new A();
        }
    }

    @Slf4j
    static class A {
        public void helloA() {
            log.info("hello A");
        }
    }

    @Slf4j
    static class B {
        public void helloB() {
            log.info("hello B");
        }
    }
}

그냥 빈을 등록하는 과정이다. B instance가 빈으로 등록이 안된것을 확인할 수 있다.

빈 후처리기를 통해 A로 바꿔치기 해보자

 

    @Test
    void basicConfig() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);

        //beanA 이름으로 B 객체가 빈으로 등록된다.
        B b = applicationContext.getBean("beanA", B.class);
        b.helloB();

        //A는 빈으로 등록되지 않는다.
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
    }

    @Slf4j
    @Configuration
    static class BeanPostProcessorConfig {
        @Bean(name = "beanA")
        public A a() {
            return new A();
        }

        @Bean
        public AToBPostProcessor helloPostProcessor() {
            return new AToBPostProcessor();
        }
    }

    @Slf4j
    static class A {
        public void helloA() {
            log.info("hello A");
        }
    }

    @Slf4j
    static class B {
        public void helloB() {
            log.info("hello B");
        }
    }

    @Slf4j
    static class AToBPostProcessor implements BeanPostProcessor {

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("beanName={} bean={}", beanName, bean);
            if (bean instanceof A) {
                return new B();
            }
            return bean;
        }
    }

 

BeanPostProcess를 구현한 instance 를 bean으로 등록하면 스프링이 빈 후처리기로 인식을 하고 동작을 하게 된다.

postProcessAfterInitialization 인자로 A instance가 들어오면 바로 B를 return 한다.

container에서 A를 등록하는 순간 B가 등록이 된다. 그래서 getBean으로 beanA를 하면 B를 정상적으로 받을 수 있는것이다.

 

스프링 자체적으로 CommonAnnotationBeanPostProcessor 를 bean으로 등록을 하고 @PostConstruct 가 붙은 메소드를  통해서 해당클래스가 빈으로 만들어지고 후 처리를 하게 된다.

 


 

적용

public class BeanPostProcessorConfig {

    @Bean
    public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
        return new PackageLogTracePostProcessor("hello.proxy.app", getAdvisor(logTrace));
    }

    private Advisor getAdvisor(LogTrace logTrace) {
        //pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

Advisor를 만들어서 BeanPostProcessor가 쓸 수 있게 주입해준다.

public class PackageLogTracePostProcessor implements BeanPostProcessor {

    private final String basePackage;
    private final Advisor advisor;

    public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
        this.basePackage = basePackage;
        this.advisor = advisor;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        log.info("param beanName={} bean={}", beanName, bean.getClass());

        //프록시 적용 대상 여부 체크
        //프록시 적용 대상이 아니면 원본을 그대로 진행
        String packageName = bean.getClass().getPackageName();
        if (!packageName.startsWith(basePackage)) {
            return bean;
        }

        //프록시 대상이면 프록시를 만들어서 반환
        ProxyFactory proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(advisor);

        Object proxy = proxyFactory.getProxy();
        log.info("create proxy: target={} proxy={}", bean.getClass(), proxy.getClass());
        return proxy;
    }
}

밖에서 주입받은 advisor를 통해 해당 빈이 만들어질때 proxy를 빈으로 등록하게 된다.

 

@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
    return new PackageLogTracePostProcessor("hello.proxy.app", getAdvisor(logTrace));
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    log.info("param beanName={} bean={}", beanName, bean.getClass());

    //프록시 적용 대상 여부 체크
    //프록시 적용 대상이 아니면 원본을 그대로 진행
    String packageName = bean.getClass().getPackageName();
    if (!packageName.startsWith(basePackage)) {
        return bean;
    }

    //프록시 대상이면 프록시를 만들어서 반환
    ProxyFactory proxyFactory = new ProxyFactory(bean);
    proxyFactory.addAdvisor(advisor);

    Object proxy = proxyFactory.getProxy();
    log.info("create proxy: target={} proxy={}", bean.getClass(), proxy.getClass());
    return proxy;

이곳에 spring이 base package에 등록한 모든 bean들이 거치게 된다. 우리가 원하는 것만 getPackageName을 통해 필터링한다.

이것을 인제 controller, service, repository 단에 적용하게 되면

기존에는 3개의 클래스 모두 따로 Proxy를 생성하는 로직이 존재하였지만 그것이 모두 PostBeanProcessor에 들어가게 되었다.

 


 

스프링에서 AOP

gradle 에 spring-bot-start-aop 를 추가하게 되면 @EnableAspectJAutoProxy 같은것을 미리 다 켜준다.

AutoProxyCreator 를 스프링 빈으로 등록하게 해주는데 이것이 빈 후처리기이다.

빈 후처리기 에서 스프링 커넽이너에서 모든 Advisor를 조회한다.
포인트컷을 통해 적용대상인지 판별하고 1 개의 메소드라도 만족한다면 프록시 객체를 생성하여 빈으로 등록한다.

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {

//    @Bean
    public Advisor advisor1(LogTrace logTrace) {
        //pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

//    @Bean
    public Advisor advisor2(LogTrace logTrace) {
        //pointcut
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* hello.proxy.app..*(..))");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

    @Bean
    public Advisor advisor3(LogTrace logTrace) {
        //pointcut
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

}

@Configuration에 Advisor만 빈으로 등록해놓으면 Spring boot 에서 미리등록해놓은 postBeanProcessor 가 advisor를 보고 프록시를 만들지 말지 판단하여 프록시를 알아서 만들게 된다.

프록시가 만들어졌어도 나중에 실제 호출단계에서 한번더 advisor의 pointcut을포고 적용할지 말지 판단을 하게 된다.

 

 

 

 

 

728x90