Backend/Spring

스프링의 빈 후처리기

Bombo_ 2023. 4. 24. 01:50
728x90

빈 후처리기라고 얘기를 하면 먼저 떠오르는 것은 @PostConstruct가 떠오를 것이다. 빈을 초기화하는데에 있어서, 초기화 이후 어떠한 작업을 추가해서 하기 위한 애노테이션인데 이 또한 빈 후처리기에 속한다.

 

오늘은 스프링의 빈 후처리기가 무엇인지 살펴보자.

Bean PostProcessor(빈 후처리기)

빈 후처리기빈 저장소에 사용될 목적으로 생성할 객체를 빈 저장소에 등록되기 직전에 조작하는 것이다.

따라서, 빈 객체가 스프링 컨테이너에 등록되기 전에 저장 될 객체를 다른 객체로 바꾼다거나, 프록시를 적용한다거나 여러가지가 가능하다.

 

빈 후처리기 적용 과정

빈 후처리기의 적용 과정은 크게 4가지로 구분 할 수 있다.

1. 생성 : 스프링 빈 대상이 되는 객체를 생성한다.

2. 전달 : 생성된 객체를 빈 후처리기에 전달한다.

3. 후 처리 작업 : 후 처리기가 스프링 빈 객체를 조작한다.

4. 등록 : 빈 후처리기 작업 이후 빈을 스프링 컨테이너에 등록한다.

이를 그림으로 보면 다음과 같다.

빈 후처리기 진행 과정

후 처리 작업을 하는 부분이 가장 큰 포인트이다. 해당 작업에서 객체가 일련의 작업으로 부가 기능이 추가되거나 변경이 발생할 수 있기 때문이다.

빈 후처리기 구현

빈 후처리기 구현은 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;
	}

}

실제 BeanPostProcessor의 인터페이스 내부를 살펴보면 다음과 같은 2개의 메서드를 볼 수 있다. default 키워드를 사용함으로써 특별히 오버라이딩 하지 않으면 객체 그대로를 반환한다.

 

메서드 2개의 사용 시기는 다음과 같다.

1. postProcessBeforeInitialization() : InitializingBean 적용 이전, @PostConstruct 적용 이전에 적용되는 메서드이다.

2. postProcessAfterInitialization() : InitializingBean 적용 이후, @PostConstruct 적용 이후에 적용되는 메서드이다.

 

그리고 2개의 매개변수는 동일한데

1. bean : 등록하는 새로운 빈 인스턴스

2. beanName : 빈의 이름

이렇게 두 가지를 매개변수로 가지고 있다. 따라서 기존의 프록시 패턴을 사용할 때 무조건 추가해야했던 target 을 적용 시 알아서 판단해서 적용해준다.

 

실제로 빈 객체가 등록되기 전에 객체를 교체하는 예시를 살펴보자.

@Slf4j
@Configuration
static class BeanProcessorConfig {
    @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) throws BeansException {
        log.info("beanName={} bean={}", beanName, bean);
        if (bean instanceof A) {
            return new B();
        }
        return bean;
    }
}

BeanPostProcessor 인터페이스를 구현하여 빈이 등록될 때 Object가 A로 형 변환이 가능하다면, B 객체로 빈을 등록하는 빈 후처리기를 볼 수 있다. (기존에 A 클래스와 B 클래스는 존재한다.)

@Test
void basicConfig() {
    // 스프링 컨테이너 생성
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanProcessorConfig.class);

    // beanA 이름으로 B 객체가 빈으로 등록
    Object bean = applicationContext.getBean("beanA");
    if(bean instanceof B) {
        bean = (B) bean;
    }
    log.info("bean {}", bean.getClass());

    // A 클래스로 등록했지만, B 클래스로 변경하여 테스트 성공
    Assertions.assertThatThrownBy(() -> applicationContext.getBean(A.class))
            .isInstanceOf(NoSuchBeanDefinitionException.class);
}

// 주요 로그
beanName=beanA bean=hello.proxy.postprocessor.BeanPostProccessorTest$A@3a0807b7
bean class hello.proxy.postprocessor.BeanPostProccessorTest$B

bean 생성 시 이름을 beanA로 지정을 해주었고, 해당 이름으로 빈 객체를 반환한다. 그리고 해당 Object 객체가 B 객체로 형변환이 가능하다면 B 객체로 변환하고, 클래스 정보를 확인하는 로그와 등록된 객체가 A 클래스가 아닌지 확인하는 테스트 코드가 있다.

 

로그를 살펴보면 빈 처리기에 실행된 로그는 분명 내부클래스 A를 등록한 것이 분명한데, 적용 이후 출력하는 것을 보면 B에 대한 클래스 정보가 나타나는 것을 볼 수 있다. 빈 후처리기가 정확하게 동작한 것이다. 정말 B 인스턴스인지 의문일 수 있기 때문에 실제로 A 클래스에 대한 테스트를 진행하여도 통과하는 것을 볼 수 있다. 정말 쉽게 중간에서 빈을 가로채서 정보를 수정할 수 있게 되었다.

또한 빈 후처리기의 가장 큰 장점은 @Component 를 사용하여 빈을 생성하는 컴포넌트 스캔에도 적용 할 수 있다는 것이다. 

하지만 주의해야 할 점 또한 존재한다.

빈 후처리기 주의사항

빈 후처리기는 스프링 컨테이너에 빈을 등록할 때 적용을 하게 되는데 범위를 지정하지 않으면(포인트 컷을 사용하지 않으면) 스프링 컨테이너에 등록되는 모든 빈에 대해 빈 후처리기가 적용된다.

 

이는 굉장히 위험한게 스프링 부트는 개발자가 직접 빈을 등록하는 것을 제외하고도 스프링을 실행 할 때 스프링 부트가 자체적으로 등록하는 스프링 빈이 엄청 많다. 이와 같은 상황에 빈 후처리기에 프록시가 적용된다면, 실제로 적용되지 못하는 클래스들도 존재하기 때문에 예외가 발생할 수 있다는 문제가 발생하는 것이다.

 

따라서, 빈 후처리기 사용 시에는 적절한 필터링 즉, 포인트컷을 해주는 것이 중요하다.

빈 후처리기는 총 2번의 필터를 거치는데 이는 다음과 같다.

 

1. 프록시 생성단계 : 클래스와 메서드에 프록시를 생성할지에 대해서 검사한다. 이 때, 모든 메서드를 검사하면서 하나라도 빈 후처리기가 적용되어야 한다면 프록시를 생성한다.

2. 어드바이스 적용단계 : 프록시 생성 단계를 통해 프록시가 생성되었을 때, 포인트컷을 재 확인하여 어드바이스를 적용 할지에 대해서 확인을 해준다. 

 

스프링의 자동 빈 후처리기

우리는 위에서 빈 후처리기를 직접 구현했지만, 스프링부트는 빈 후처리기를 자동으로 해주는 것이 존재한다.

AnnotationAwareAspectJAutoProxyCreator 클래스 다이어그램

바로 AnnotationAwareAspectJAutoProxyCreator라는 클래스이고, 실제로 다이어그램을 살펴보면 BeanPostProcessor를 인터페이스로 가지고 있는 것을 볼 수 있다. 

 

이 클래스는 빈으로 Advisor를 등록하면 해당 Advisor를 보고 빈 후처리기가 직접 등록을 해주고 만약 @Aspect 애노테이션을 사용하여 등록한다면 BeanFactoryAspectJAdvisorsBuilder라는 내부 캐시에 저장하여 호출 할 수 있도록 해준다.

@Aspect는 후에 AOP를 설명할 때 후에 포스팅 해보려고 한다.

 

그럼 자동 빈 후처리기가 어떻게 등록되는지 살펴보자.

자동 빈 후처리기 적용 과정

1. 생성 : 수동이든 컴포넌트 스캔이든 빈을 등록하기 위한 객체를 생성한다.

2. 전달 : 생성 된 객체를 자동 빈 후처리기에 전달한다.

3. 조회 : Advisor 빈과 @AspectJ가 있는지 확인한다.

4. 프록시 생성 : PointCut을 통해서 적절하게 프록시를 생성한다.

5. 등록 : 스프링 컨테이너에 생성된 프록시를 전달하고 등록한다. 

자동 빈 후처리기 적용 과정

이제 이를 기반으로 다음 포스팅에는 스프링 AOP가 어떻게 동작하는지 살펴보도록 하자.

 

위 내용은 Inflearn 김영한님의 스프링 핵심원리 고급편 강의를 기반으로 작성되었습니다.