[Back-end]/[Spring]

[Spring] 빈 후처리기(BeanPostProcessor)

DevLoki 2021. 12. 5. 03:17

이전 글에서는 프록시 팩토리의 장점과 한계점에 대해 알아보았습니다.

 

[Spring] 프록시 팩토리(ProxyFactory)

이전 포스팅에서 다이내믹 프록시의 단점을 정리하며 마무리하였습니다. [Spring] 다이내믹 프록시(DynamicProxy) (프록시에 대한 이해가 부족하신 분들은 이전 포스팅을 참고하세요!!) [Spring] 프록시

yejun-the-developer.tistory.com

한계점을 다시 정리해보자면 다음과 같습니다.

  • 프록시를 적용할 스프링 빈의 갯수만큼 프록시를 생성하여 빈으로 등록해주어야 한다. (설정 파일이 관리가 힘들다.)
  • 컴포넌트 스캔으로 등록된 스프링 빈에는 적용할 수 없다.

이러한 문제들을 해결하기 위해 실무에서는 빈 후처리기(BeanPostProcessor)를 사용합니다.

일반적으로 @Bean 이나 @Component를 사용하여 빈등록을 하면 스프링은 대상 객체를 생성하고, 스프링 컨테이너 내부의 빈 저장소에 등록합니다.

일반적인 빈 등록 과정

하지만 빈등록을 하기 전에 빈을 원하는대로 조작할 수 있는 기능을 제공해주는 것이 바로 빈 후처리기입니다.

빈 후처리기 적용 후 빈등록

빈 후처리기(BeanPostProcessor)

생성 방법

빈후처리기는 BeanPostProcessor 인터페이스를 구현하여 생성합니다.

BeanPostProcessor의 구조는 다음과 같습니다.

public interface BeanPostProcessor {

	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

}

두 메서드 중 하나의 메서드만 오버라이딩하여 사용할 수 있습니다. 두 메서드의 차이점은 빈 후처리기의 후처리기 시점입니다.

  • postProcessBeforeInitialization() : 객체 생성 후, 초기화 작업 이전에 후처리를 진행한다.
  • postProcessAfterInitialization() : 객체 생성 후, 초기화 작업 이후에 후처리를 진행한다.

예시

package blog.proxy.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
public class PostProcessor {

    @Test
    void postProcessor(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(PostProcessorConfig.class);
        B beanA = ac.getBean("beanA", B.class);
        beanA.run();
    }

    @Configuration
    static class PostProcessorConfig {

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

        @Bean(name = "beanB")
        public B b(){
            return new B();
        }

        @Bean
        public AToBBeanPostProcessor beanPostProcessor(){
            return new AToBBeanPostProcessor();
        }
    }

    static class A{
        void run() {
            log.info("A.run() 실행");
        }
    }

    static class B{
        void run(){
            log.info("B.run() 실행");
        }
    }

    static class AToBBeanPostProcessor implements BeanPostProcessor{
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("[빈후처리기 실행] bean = {}, beanName = {}", bean, beanName);
            if (bean instanceof A) {  //bean 이 class A의 인스턴스라면
                return new B();  //새로운 B 객체를 생성해서 반환한다.
            }
            return bean;
        }
    }

}

각 스프링빈 생성 과정은 다음과 같습니다.

beanA 빈 등록 과정
beanB 빈 등록 과정

결과

ApplicationContext.getBean() 을 이용해 빈이름(beanA)으로 빈을 가져와 run()메서드를 실행했지만, 출력된 로그는 "B.run() 실행" 이었습니다.

이처럼, 빈후처리기를 통해 등록할 빈을 변경 및 조작할 수 있습니다. 일반적으로 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 수 있는 방법이 존재하지 않지만, 이 방법을 이용하면 개발자가 등록하는 모든 빈을 조작할 수 있습니다.

빈후처리기 프록시에 적용하기

빈 후처리기를 사용하여 실제 객체 대신 프록시를 스프링 빈으로 등록해보겠습니다.

동작 방식은 다음과 같습니다.

빈후처리기 프록시에 적용하기

예시

Target.java

@Slf4j
public class Target {

    public void run(){
        log.info("Target.run() 실행");
    }

}
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class MyAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        log.info("[MyAdvice] 부가기능 실행");
        Object result = invocation.proceed();
        log.info("[MyAdvice] 부가기능 실행");

        return result;
    }
}

ProxyBeanPostProcessor.java

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class ProxyBeanPostProcessor implements BeanPostProcessor {

    private Advisor advisor;

    public ProxyBeanPostProcessor(Advisor advisor) {
        this.advisor = advisor;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        String packageName = bean.getClass().getPackageName();  //실제 대상 오브젝트 클래스의 패키지 경로
        if (!packageName.startsWith("blog.proxy.postprocessor")) {  //해당 경로의 클래스 오브젝트에만 빈 후처리기 적용
            return bean;
        }
        ProxyFactory proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(advisor);
        Object proxy = proxyFactory.getProxy();

        return proxy;
    }
}

PostProcessorConfig.java

import org.springframework.aop.Pointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PostProcessorConfig {

    @Bean
    public Target target(){
        return new Target();
    }

    @Bean
    public DefaultPointcutAdvisor advisor(){
        MyAdvice myAdvice = new MyAdvice();
        return new DefaultPointcutAdvisor(Pointcut.TRUE, myAdvice);
    }

    @Bean
    public ProxyBeanPostProcessor proxyBeanPostProcessor(){
        return new ProxyBeanPostProcessor(advisor());
    }
}
  • Target.java : 프록시 객체의 실제 대상이 되는 클래스로 간단한 로그를 출력하는 run() 메서드를 가지고 있습니다.
  • MyAdvice.java : 실제 대상 오브젝트 요청 위임 전후에 로그를 출력해주는 어드바이스입니다.
  • ProxyBeanPostProcessor.java : 내부에 프록시 팩토리를 사용하여 입력으로 받은 빈 객체를 프록시로 변경하여 반환합니다. 빈 후처리기를 빈으로 등록하면, 빈으로 등록되는 모든 오브젝트에 대해 후처리 작업을 진행합니다. 만약 메서드에 final 키워드가 붙어있다면 스프링은 오류가 발생합니다. 따라서, "blog.proxy.postprocessor" 의 하위 경로에 있는 클래스 오브젝트에만 적용되도록 패키지 경로를 검사하는 코드를 추가하였습니다.
  • PostProcessorConfig.java : 위 3개의 클래스를 빈으로 등록합니다.

테스트 코드

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class PostProcessorTest {

    @Autowired
    Target target;

    @Test
    void postProcessor(){
        target.run();

        log.info("target.getClass = {}", target.getClass());
        log.info("target.isAopProxy = {}", AopUtils.isAopProxy(target));
    }

}

결과