개발놀이터

빈 후처리기 본문

Spring/Spring

빈 후처리기

마늘냄새폴폴 2022. 1. 20. 16:19

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 고급 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요

 

빈 후처리기란 무엇일까?

 

스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하는 기능이다. 

 

그림으로 간단하게 알아보자

1. A객체를 생성하고

2. A객체를 빈 후처리기에 전달한다.

3. A객체를 B객체로 바꿔치기하고

4. 스프링 빈 저장소에 B객체를 저장한다.

 

 

이해를 돕기위해 예제코드로 살펴보자

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");
        }
    }
}

A객체와 B객체를 하나씩 만들고 A객체를 빈으로 등록했다. 그럼 A는 빈으로 등록되고 B는 등록되지 않는다. 당연한 결과이다.

 

이제 A객체가 저장되는것 대신 B객체를 저장해보자

 

A객체 대신 B객체를 저장하려면 빈 후처리기를 사용해야 하는데 빈 후처리기를 사용하는 방법은 다음과 같다.

 

1. BeanPostProcessor 인터페이스를 구현하고 스프링 빈으로 등록한다.

2. postProcessBeforeInitialization 혹은 postProcessAfterInitialization을 구현한다.

 

-postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct같은 초기화가 발생하기 전에 호출되는 포스트 프로세스이다.

-postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct같은 초기화가 발생한 다음에 호출되는 포스트 프로세스이다. 

 

public class BeanPostProcessorTest {

    @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;
        }
    }
}

BeanPostProcessor를 구현한 부분을 잘 보면 인자값으로 bean과 beanName이 들어오고 bean이 A의 인스턴스이면 B객체를 리턴하여 스프링 빈에 등록하는 모습을 볼 수 있다. A인스턴스가 아니라면 그냥 bean을 리턴한다.

 

이제 실제로 적용해보자

 

PackageLogTracePostProcessor.java

@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {

    private final String baskPackage;
    private final Advisor advisor;

    public PackageLogTracePostProcessor(String baskPackage, Advisor advisor) {
        this.baskPackage = baskPackage;
        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(baskPackage)) {
            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;
    }
}

이 객체는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 이때 프록시 팩토리를 사용하는데 프록시 팩토리는 어드바이저가 필요하기 때문에 이 부분은 외부에서 주입받도록 했다.

 

모든 스프링 빈들에 프록시를 적용할 필요는 없다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. 여기서는 hello.proxy.app과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.

 

프록시 적용 대상의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환한다. 따라서 스프링 컨테이너에 원본 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 원본 객체는 스프링 빈으로 등록되지 않는다.

 

@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
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);
    }
}

이제 프록시를 생성하는 코드가 설정 파일에는 필요 없다. 순수한 빈 등록만 고민하면 된다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.

'Spring > Spring' 카테고리의 다른 글

스프링 AOP - 포인트컷  (0) 2022.01.24
@Aspect  (0) 2022.01.20
포인트컷, 어드바이스, 어드바이저  (0) 2022.01.19
프록시 팩토리  (0) 2022.01.19
동적 프록시  (0) 2022.01.18