Spring Advanced

빈 후처리기(BeanPostProcessor)

cwchoiit 2023. 12. 27. 21:36
728x90
반응형
SMALL
728x90
SMALL

스프링에서 '@Bean'이나 컴포넌트 스캔으로 스프링 빈을 등록하면 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 그리고 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용한다.

 

스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용한다.  

BeanPostProcessor는 번역하면 빈 후처리기로, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.

 

빈 후처리기는 강력하다. 객체를 조작하는게 가능하고 완전히 다른 객체로 바꿔치기 하는 것도 가능하다. 빈 후처리기 과정을 자세히 살펴보자.

 

빈 후처리기 과정

스프링 빈 등록 과정 -> 빈 후처리기

 

1. 생성: 스프링 빈 대상이 되는 객체를 생성한다 (@Bean, 컴포넌트 스캔 모두 포함)

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.

3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 할 수 있다.

4. 등록: 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

 

여기서 '3. 후 처리 작업'을 보면 스프링 빈 객체를 조작 또는 바꿔치기 한다고 되어 있는데 이 말은 무슨 말일까? 다음 그림을 보자.

 

빈 후처리기에서 객체 A를 객체 B로 바꿔버린 모습을 볼 수 있다. 그리고 그 바꾼 객체 B를 스프링 빈 저장소에 전달하면 최초 객체 A가 객체 B로 최종 등록된다. 이것을 객체를 조작 또는 바꿔치기한다 말한다.

 

빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.

이 BeanPostProcessor 인터페이스는 두 개의 메서드를 제공한다. 

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

 

이 빈후처리기를 통해 특정 객체를 다른 객체로 변경해버리는 예시 코드를 작성해보자.

 

빈 후처리기 테스트 코드

BeanPostProcessorTest.java

package com.example.advanced.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
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;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

@Slf4j
public class BeanPostProcessorTest {

    @Test
    void basicConfig() {
        // AnnotationConfigApplicationContext 자체가 스프링 컨테이너
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(beanPostProcessorConfig.class);

        // A를 Bean 으로 등록했지만 빈 후처리기에서 A를 B로 바꿨다.
        B b = applicationContext.getBean("beanA", B.class);
        b.helloB();

        // 그러므로 A는 Bean 으로 등록되지 않는다.
        assertThatThrownBy(() -> applicationContext.getBean(A.class))
                .isInstanceOf(NoSuchBeanDefinitionException.class);
    }

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

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

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

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

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

 

위 코드를 보면 클래스 A, 클래스 B가 있고 AtoBPostProcessor라는 클래스가 BeanPostProcessor를 구현한다. 이 AtoBPostProcessor 클래스에서는 BeanPostProcessor의 메서드 중 하나인 postProcessAfterInitialization을 구현한다. 이 메서드에서 넘겨받는 빈의 타입이 'A'인 경우 'B'타입으로 변경하여 반환한다.

 

그리고 실제로 A를 빈으로 등록하는 Configuration 클래스로 빈을 등록한 후 스프링 컨테이너에서 A객체를 빈으로 등록할 때 사용한 이름인 'beanA'를 가져오면 B 객체를 반환 받는것을 확인할 수 있다.

 

 

정리를 하자면..

이렇게 빈 후처리기를 통해 스프링이 빈 저장소(스프링 컨테이너)에 등록할 객체를 강력한 방식으로 조작하고 변경할 수 있다. 여기서 조작이란 메서드를 호출함을 의미한다. 예를 들어, @PostConstruct 애노테이션이 붙은 메서드도 스프링이 자체적으로 등록하는 빈 후처리기인 'CommonAnnotationBeanPostProcessor'에서 해당 애노테이션이 붙은 객체를 다 찾아내서 해당 메서드를 호출하는 것이다. 

 

이 말은, 컴포넌트 스캔으로 등록한 빈도 개발자는 조작 및 바꿔치기가 가능하다는 얘기다. 그리고 실제로 스프링은 AOP를 구현할 때 빈 후처리기를 통해 컴포넌트 스캔으로 등록되는 빈 중 프록시로 만들어져야 하는 객체를 포인트컷을 통해 찾아 프록시로 변경하여 등록해준다. 

 

그렇다면 스프링이 제공하는 빈 후처리기는 어떤것들이 있을까?

 

스프링이 제공하는 빈 후처리기

우선, 스프링이 제공하는 빈 후처리기를 사용하기 위해서는 다음 라이브러리를 추가해야한다.

implementation 'org.springframework.boot:spring-boot-starter-aop'

 

이 라이브러리를 추가하면 'aspectjweaver' 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다. 스프링 부트가 활성화하는 빈은 AopAutoConfiguration인데 이 빈을 활성화하면 자동 프록시 생성기라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.

 

 

AnnotationAwareAspectJAutoProxyCreator

이 녀석이 스프링 부트가 자동으로 스프링 빈으로 등록해주는 빈 후처리기다. 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기. 이 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다. Advisor안에는 Pointcut과 Advice가 이미 모두 포함되어 있다. 따라서 Advisor만 알고 있으면 그 안에 있는 Pointcut으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다. 그리고 Advice로 부가 기능을 적용하면 된다.

 

그리고 @Aspect도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.

 

 

자동 프록시 생성기의 작동 과정을 자세히 살펴보자.

 

1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.

3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다.

4. 프록시 적용 대상 체크: 앞서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이 때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 하나라도 조건이 만족하면 프록시 적용 대상이 된다. 예를 들어 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.

5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.

 

 

이를 통해 한가지 더 알 수 있는 중요한 사실이 있다.

포인트컷은 2가지에 사용된다.

1. 프록시 적용 여부 판단 - 프록시를 생성하는 단계

  • 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
  • 클래스 + 메서드 조건을 모두 비교한다. 이 때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성하고, 조건에 맞는 것이 하나도 없을 땐 프록시를 생성하지 않는다.

2. 어드바이스 적용 여부 판단 - 프록시를 사용하는 단계

  • 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다. 즉, 특정 메서드가 호출됐을 때 그 메서드가 포인트컷 조건에 만족하는 메서드인지 확인 한다는 뜻이다.

프록시를 모든 곳에 생성하는 것은 비용 낭비이기 때문에 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 게 아니고 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.

 

프록시를 모든 곳에 생성하는 것은 비용 낭비라고 했고 자동 프록시 생성기는 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다고 했다. 그럼 만약 여러개의 포인트컷 조건을 만족한다고 하면 프록시는 여러개가 생길까? 아니다. 프록시는 딱 하나만 생기고 여러 어드바이저가 생긴다.

 

프록시 자동 생성기 상황별 정리

  • advisor1의 포인트컷만 만족: 프록시 1개 생성, 프록시에 advisor1만 포함
  • advisor1, advisor2의 포인트컷을 모두 만족: 프록시 1개 생성, 프록시에 advisor1, advisor2 모두 포함
  • advisor1, advisor2의 포인트컷을 모두 만족하지 않음: 프록시가 생성되지 않음

 

 

728x90
반응형
LIST

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

AOP(Aspect Oriented Programming)  (0) 2023.12.29
AOP와 @Aspect, @Around  (0) 2023.12.29
Advisor, Advice, Pointcut  (0) 2023.12.15
스프링이 지원하는 ProxyFactory  (0) 2023.12.14
Proxy/Decorator Pattern 2 (동적 프록시)  (0) 2023.12.14