본문 바로가기
Java/Spring

빈 후처리기(BeanPostProcessor)

by oneny 2023. 10. 17.

빈 후처리기

스프링에서 빈 후처리기(BeanPostProcessor)는 컨테이너에 의해 생성된 빈 객체의 초기화 과정에서 커스텀 동작을 수행할 수 있도록 해주는 용도로 사용된다. 빈 후처리기는 스프링의 AOP(Aspect-Oriented Programming)와 관련된 작업, 빈 초기와나 소멸 시 추가 작업, 그리고 빈의 커스텀 로직 적용과 같은 다양한 작업을 수행할 때 유용하다.

 

BeanPostProcessor 인터페이스

  • postProcessBeforeInitialization(Object bean, String beanName): 빈 객체가 초기화되기 전에 호출되는 메서드이다. 이 메서드를 통해 빈 객체를 수정하거나 빈 객체가 초기화되기 전 추가 작업을 수행할 수 있다.
  • postProcessAfterInitialization(Object bean, String beanName): 빈 객체가 초기화된 후에 호출되는 메서드이다. 이 메서드를 사용하여 빈 객체를 수정하거나 초기화 이후 추가 작업을 수행할 수 있다.

빈 후처리기를 사용한다면 스프링 빈 대상이 되는 객체를 생성하고 빈 저장소에 등록하기 전에 빈 후처리기에 전달하여 추가 동작을 수행할 수 있고, 다른 객체로 완전히 바꿔치기하는 것도 가능하기 때문에 빈 후처리기의 기능은 매우 막강하다.

 

빈 후처리기 과정

  1. 생성: 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)
  2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 할 수 있다.
  4. 등록: 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

 

@PostConstruct의 비밀

@PostConstruct는 스프링 빈 생성 이후에 빈을 초기화하는 역할을 한다. 즉, 빈의 초기화라는 것이 단순히 @PostConstruct 애노테이션이 붙은 초기화 메서드를 한 번 호출만 하면 된다. 따라서 빈을 조작하는 행위를 하는 적절한 빈 후처리기가 있으면 되는데 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록하고, 여기에서 @PostConstruct 어노테이션이 붙은 메서드를 호출한다. 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다.

 

빈 후처리기 예제

public class BasicTest {

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

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

       // A는 빈으로 등록되지 않는다.
       assertThatThrownBy(() -> applicationContext.getBean(A.class))
          .isInstanceOf(NoSuchBeanDefinitionException.class);

    }

    @Slf4j
    @Configuration
    static class BasicConfig {

       @Bean(name = "beanA")
       public A a() {
          return new A();
       }

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

    @Slf4j
    static class AtoBPostProcessor implements BeanPostProcessor {

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

          if (bean instanceof A) {
             return new B();
          }

          return bean;
       }
    }

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

    @Slf4j
    static class B {
       public void helloB() {
          log.info("hello B");
       }
    }
}
  • new AnnotationConfigApplication(BasicConfig.class): 스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주었다. BasicConfig.class 설정 파일은 스프링 빈으로 등록된다.
  • AtoBPostProcessor: BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.
    • 이 빈 후처리기는 A 객체를 새로운 B 객체로 바꿔치기 한다. 파라미터로 넘어오는 빈 객체가 A의 인스턴스이면 새로운 B 객체를 생성해서 반환한다. 여기서 A 대신에 반환된 값인 B가 스프링 컨테이너에 등록된다. 따라서 아래 결과처럼 최종적으로 "beanA"라는 스프링 빈 이름에 B 객체가 등록됐기 때문에 helloB 메서드가 실행된 것을 확인할 수 있다.

 

프록시팩토리와 빈 후처리기

이전 게시글에서 수 많은 설정 파일과 컴포넌트 스캔으로는 프록시팩토리를 사용할 수 없는 문제를 빈 후처리기로 해결할 수 있다고 했다. 즉, 빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록할 수 있다. 이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있고, 설정 파일에 있는 반복적인 수 많은 프록시 생성 코드도 한 번에 제거할 수 있다.

 

 

프록시팩토리와 빈 후처리기 예제

패키지 구조는 이전 프록시팩토리에서 사용했던 Service, ServiceImpl, TimeAdvice를 app 패키지에 나눠 저장했다. 그리고 BeanPostProcessor를 구현한 PackageTimeProxyPostProcessor를 만들고 PostProcessorTest를 통해 테스트해보자.

 

@Slf4j
public class PackageTimeProxyPostProcessor implements BeanPostProcessor {

    private final String basePackage;
    private final Advisor advisor;

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

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        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;
    }
}

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

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

 

@Slf4j
public class PostProcessorTest {

    @Test
    public void postProcessorConfigTest() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PostProcessorConfig.class);

        Service service = applicationContext.getBean("service", Service.class);

        service.save();
        service.find();
    }

    @Slf4j
    @Configuration
    static class PostProcessorConfig {

        @Bean
        public Service service() {
            return new ServiceImpl();
        }

        @Bean
        public PackageTimeProxyPostProcessor packageTimeProxyPostProcessor() {
            return new PackageTimeProxyPostProcessor("oneny.calculation.proxy.app", getAdvisor());
        }

        private Advisor getAdvisor() {
            // pointcut
            NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
            pointcut.setMappedNames("request*", "save*");

            // advisor = pointcut + advice
            return new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
        }
    }
}
  • @Bean packageTimeProxyPostProcessor(): 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다. 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다. 여기에 프록시를 적용할 패키지 정보와 어드바이저를 넘겨준다.

이제 프록시를 생성하는 코드가 설정 파일에는 필요가 없어졌다. 순수한 빈만 등록하면 빈 후처리기를 통해서 프록시 객체를 스프링 빈 저장소에 등록할 수 있다. 따라서 Configuration 파일에 각 빈마다 프록시를 적용해야 하는 문제나 컴포넌트 스캔을 사용하는 경우에는 프록시를 사용할 수 없는 문제를 빈 후처리기를 통해 해결하였다.

 

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

 

출처

스프링 핵심 원리 - 고급편

 

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

JPA  (0) 2024.03.26
회원가입 동시성 이슈 테스트 - DB Unique 활용하기  (0) 2023.10.30
프록시 팩토리를 통한 AOP  (1) 2023.10.16
Auto Configuration  (0) 2023.10.09
ThreadLocal을 사용하여 로깅하기 + MDCFilter  (0) 2023.10.08