본문 바로가기
Framework & Lib & API/스프링

스프링 빈 생성 과정 분석 [3] - BeanFactoryPostProcessor, BeanPostProcessor

by 코딩공장공장장 2024. 4. 20.

이전 포스팅에서 BeanDefinitionRegsitry와 SingletonRegistry에 관련하여 빈 생성과정에 대해 설명하였다.

 

이번 포스팅에서는 아래 굵은 글씨로 밑줄친 내용에 대해 알아볼 것이다.

 

     1) 생성할 빈의 BeanDefition 정의

     2) BeanFactoryPostProcessor가 BeanDefition을 BeanDefinitionResitry에 저장

     3) BeanDefition을 바탕으로 빈 인스턴스화 (스프링 default 빈 생성 방식은 싱글톤)
          3-1) 인스턴스화 이후 BeanPostProcessor가 빈 인스턴스의 후처리 작업 진행

     4) 빈 인스턴스를 SingletonBeanRegistry에 저장

 

2)번 내용 ‘BeanDefition을 BeanDefinitionResitry에 저장’하는 것은 지난 포스팅에서도 다루었지만 

이 처리를 담당하는 BeanFactoryPostProcessor를 소개하고 구동시점에 어떻게 모든 빈의 BeanDefinition을 등록하는지 알아보겠다.

 

3-1)번 내용은 지난 포스팅에서도 언급하지 않았던 내용이다. 

빈은 인스턴스화 이후 바로 사용할 수 있는 수준이 아니다. 

이로인해 몇몇 후처리 작업이 필요하고 이를 BeanPostProcessor가 담당한다.
이 작업에 무엇이 있는지 그리고 구현체들이 어떤 작업을  처리하는지 알아보겠다.

 

BeanFactoryPostProcessor, BeanPostProcessor


1.  BeanFactoryPostProcessor

1.1 BeanFactoryPostProcessor(spring docs

public interface BeanFactoryPostProcessor {

   void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}

BeanFactoryPostProcessor는 BeanDefinition 변경(modify)의 책임을 갖는다.

허나, BeanDefinition 변경만할 뿐 절대 인스턴스화하지 않는다.

인스턴스화는 빈팩토리에서 담당한다.(getBean 메서드 안의 createBean 메서드)

 

1.2 BeanDefinitionRegistryPostProcessor(spring docs)

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {

   void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;

 

   default void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

   }

}

BeanDefinitionRegistryPostProcessor는 BeanFactoryPostProcessor를 상속받은 확장된 인터페이스이다.
BeanFactoryPostProcessor 보다 일찍 실행되어 beanDefinition을 등록할 수 있다.

이는 변경(modify) 책임을 갖는 BeanFactoryPostProcessor와 달리 BeanDefinition 등록의 책임을 갖는다.

 

1.3 BeanDefinition 생성 및 등록

스프링 부트 auto-config에서 사용하는 BeanDefinitionRegistryPostProcessor의 구현체는 ConfigurationClassPostProcessor 이다.

 

[ConfigurationClassPostProcessor의 상속관계]



스프링 부트 프로젝트를 구동하면 아래와 같은 순서로 메서드들이 실행된다.

SpringApplication : run() ->  refreshContext() -> refresh()
AbstractApplicationContext :  refresh() -> invokeBeanFactoryPostProcessors()
PostProcessorRegistrationDelegate : invokeBeanFactoryPostProcessors() -> invokeBeanDefinitionRegistryPostProcessors()

 

[PostProcessorRegistrationDelegate.class]

private static void invokeBeanDefinitionRegistryPostProcessors(Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {

 

   while(var3.hasNext()) {

       // beanDefinition 생성 및 등록

       postProcessor.postProcessBeanDefinitionRegistry(registry);

 

   }

}

 

이 메서드의 인자로 전달되는 postProcessor는BeanDefinitionRegistryPostProcessor타입의 모든 빈을 조회해서 나온 구현체들이다. 

여기에 ConfigurationClassPostProcessor이 포함되어있다.

(autoConfig 방식에서는 ConfigurationClassPostProcessor 하나만 포함되어있을 것이다. 

하위에 구현체들이 많이 존재하지만 특정조건에만 생성되는 빈이라 별다른 설정을 하지 않으면 빈으로 등록되지 않는 듯하다.)

잠깐, ConfigurationClassPostProcessor는 이 시점에 이미 존재하나?

디폴트 컨텐스트인 AnnotationConfigApplicationContext의 생성자 실행
-> AnnotatedBeanDefinitionReader 생성자 실행
-> AnnotationConfigUtils의 registerAnnotationConfigProcessors 메서드 실행

-> ConfigurationClassPostProcessor의 BeanDefinition 등록

if (!registry.containsBeanDefinition("org.springframework.context.annotation.internalConfigurationAnnotationProcessor")) {
   def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
   def.setSource(source);
   beanDefs.add(registerPostProcessor(registry, def, "org.springframework.context.annotation.internalConfigurationAnnotationProcessor"));
}

스프링에서는 위와 같이 생성자와 함께 실행되는 메서드들로 필요한 BeanDefinition을 정의한다.
소스코드를 분석하다보면 위와 같이 직접 등록하는 코드들이 많이 있다.

[ConfigurationClassPostProcessor]

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {

       ... 생략

       this.processConfigBeanDefinitions(registry);

   }

}

 

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {

   List<BeanDefinitionHolder> configCandidates = new ArrayList();

   // 부트스트랩 클래스명(@SpringApplication이 붙은 메인 클래스명)

   String[] candidateNames = registry.getBeanDefinitionNames();

   

   // configCandidates에 부트스트랩 클래스 beanDefinition 하나 존재

   if (!configCandidates.isEmpty()) {

 

        ...생략

       // @Configuration 클래스 파서

       ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry);

 

       ...생략

       do {

           // @Configuration, @Component가 붙은 클래스에 대한 BeanDefinition 등록

           parser.parse(candidates);

 

           ...생략

}

 

beanDefinition을 생성하고 beanDefinitionRegistry에 등록하는 과정은 위 주석에서 설명하는 과정과 같다.

 

부트스트랩 클래스 beanDefinition이 존재 가능하는 이유는 아래와 같이 스프링 프로젝트를 구동시키는 run 함수에서 매개변수로 부트스트랩 클래스가 주입되기 때문에 이 부트스트랩 클래스에 대해서는 미리 beanDefinition을 생성할 수 있는 것이다.

 

@SpringBootApplication

public class SpringBootProject {

   public static void main(String[] args) {
      // run 함수의 매개변수로 부트스트랩 클래스 주입

       SpringApplication.run(SpringBootProject.class, args);

   }

}

 

이 부트스트랩  클래스 beanDefinition을 통해 스프링 프로젝트에 정의한 모든 빈들을 등록할 수 있다.

 

이유는 ConfigurationClassParser의 동작 원리에 있다.

  • 부트스트랩 클래스 경로는 최상위 패키지이므로  이를 통해 하위 모든 클래스 파일을 스캔할 수 있다.
  • ConfigurationClassParser는 트스트랩 하위 패키지 경로의 @Configuration을 붙은 클래스를 스캔한다.
  • ConfigurationClassParser는 ComponentScanAnnotationParser를 참조 속성으로 갖는다.
  • ComponentScanAnnotationParser는 부트스트랩 하위 패키지 경로의 @Component가 붙은 클래스를 스캔한다.
  • ConfigurationClassParser와 ComponentScanAnnotationParser 모두 ClassPathBeanDefinitionScanner의 doScan메서드를 통해 스캔한 빈후보군에 대해 beanDefinition을 등록한다.

위와 같은 방식으로 부트스트랩  beanDefinition 하나만으로 프로젝트 정의한 모든 빈들이 등록 될 수 있는 것이다.


ClassPathBeanDefinitionScanner의 doScan 메서드의 beanDefinition 등록 방식은 이전 글에도 설명하였지만 아래와 같다.

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
. . .

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {

  for(int var5 = 0; var5 < var4; ++var5) {
      String basePackage = var3[var5];

       /**
        * 1. basePackage에 @Component가 붙은 후보군 스캔
        * 스캔 과정에서 beanDefinition을 생성하여 가져온다.
        * 이때, 가져오는 beanDefinition에는 클래스타입, 어노테이션 정보들이 포함되어있다.

        */
      Set<BeanDefinition> candidates = this.findCandidateComponents(basePackage);


      while(var8.hasNext()) {

          
            // 2. 빈 스코프 정의
          candidate.setScope(scopeMetadata.getScopeName());

          if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {

          
             // 3. lazy, primary 빈 정보 등록
             AnnotationConfigUtils.processCommonDefinitionAnnotations(annotatedBeanDefinition);
          }

          if (this.checkCandidate(beanName, candidate)) {

               // 4. beanDefinitionRegistry에 beanDefinition 저장
               this.registerBeanDefinition(definitionHolder, this.registry);
          }
      }
  }

  return beanDefinitions;
}

참고로, 부트스트랩과 경로가 다른 빈들을 @ComponentScan(basePackages=”패키지 경로”)를 통해 등록할 수 있다.

 

 

[예제] - 실제 커스텀 빈 만들기

 

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

annotation class CustomBean

 

@CustomBean

class CustomClass

 

위와 같이 CustomBean이라는 커스텀 어노테이션을 만들고,
CustomClass에 @CustomBean 어노테이션을 붙여준다.

 

@Component

class CustomBeanDefinitionRegistryPostProcessor: BeanDefinitionRegistryPostProcessor {

   override fun postProcessBeanDefinitionRegistry(registry: BeanDefinitionRegistry) {

       // annotation filter 등록

       val componentScanner = ClassPathScanningCandidateComponentProvider(false)

       val annotationFilter = AnnotationTypeFilter(CustomBean::class.java)

       componentScanner.addIncludeFilter(annotationFilter)

 

       // basePackage에 대하여 빈 후보군 beanDefinition 추출

       val beanDefOfCandidates = componentScanner.findCandidateComponents("com.yojic.springstudy.beanfactory.processor")

 

       // 빈 후보들에 대하여 beanDefinition 생성 및 등록

       for (beanDef in beanDefOfCandidates) {

           val beanClass = Class.forName(beanDef.beanClassName)

           val beanName = beanClass.simpleName.replaceFirstChar { it.lowercase() }

           val beanDefBuilder = BeanDefinitionBuilder.genericBeanDefinition(beanClass)

 

           // beanDefinition 속성 정의

           beanDefBuilder.setScope("singleton")

           beanDefBuilder.setPrimary(true)

 

           // 빈 등록

           registry.registerBeanDefinition(beanName, beanDefBuilder.beanDefinition)

       }

   }

}

 

그런 다음 위와 같은 BeanDefinitionRegistryPostProcessor를 빈으로 등록해준다.

코드에 대한 설명은 주석을 참고하기를 바란다.

이제껏 설명했던 내용으로 충분히 이해할 수 있을거라고 생각한다.

 

@SpringBootTest

class CustomClassTest(

   @Autowired private val context: ApplicationContext

){

   @Test

   fun `customBean 등록 확인`(){

       val customClass = context.getBean("customClass")

       assertThat(customClass).isNotNull

   }

}

 

위  테스트 코드를 실행하면 성공한다.

context에 정상적으로 빈으로 등록 됬음을 확인할 수 있다.

 

2. BeanPostProcessor

빈 생성과정에서 이루어지는 처리가 무엇이 있는지 알아보자.

 

1. BeanDefinitionRegistryPostProcessor가 BeanDefinition을 등록하는 Post Processor Beans definition 과정을 진행한다.

 

2. getBean을 통해 bean이 존재하지 않으면 createBean을 하며 인스턴스화가 진행된다.

 

3. 인스턴스화 이후 BeanPostProcessor가 빈의 초기화 작업을 진행하는  Bean post processor 과정을 진행한다.

 

빈은 인스턴스화가 되었을때 바로 사용할 수 있는 수준이 아니다. 

인스턴스화 이후 초기화 작업이 필요한데 의존관계 설정이나 프록시 설정 등이 있다.

 

BeanPostProcessor에 초기화 과정이 끝난다면 비로소 singletonBeanRegistry에 빈이 등록이 된다.

 

2.1 BeanPostProcessor

 

public interface BeanPostProcessor {

   default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

       return bean;

   }

 

   default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

       return bean;

   }

}

 

BeanPostProcessor는 위와 같이 빈 초기화 이전, 초기화 이후에 실행되는 메서드가 존재한다.

초기화 기준은 빈에 존재하는 init 메서드이다.

 

테스트 코드를 통해 postProcessBeforeInitialization, postProcessAfterInitialization가 어떻게 동작하는지 알아보자.

 

@Component

class CustomBeanPostProcessor : BeanPostProcessor {

   override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {

       if (bean is CustomProcessorService) {

           println("before")

       }

       return bean

   }

 

   override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {

       if (bean is CustomProcessorService) {

           println("after")

       }

       return bean

   }

}



@Service

class CustomProcessorService {

   @PostConstruct

   fun init() {

       println("init")

   }

}

 

[출력]

before

init

after

 

2.2 AutowiredAnnotationBeanPostProcessor (BeanPostProcessor 구현체)

@Autowired를 통한 의존관계 주입에 사용되는 BeanPostProcessor 구현체를 보겠다.

디버깅을 하면 아래와 같이 

getBean() -> createBean() -> postProcessProperties() 메서드가 실행됨을 알 수 있다.

 postProcessProperties 메서드는 InstantiationAwareBeanPostProcessor라는 BeanPostProcessor의 하위 타입 인터페이스이다.

 

postProcessProperties 메서드를 보자.

public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
  // 빈 메타정보 생성(의존관계 객체 정보)

   InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);

 

   try {
      // 메타 정보 기반으로 의존 주입

       metadata.inject(bean, beanName, pvs);

       return pvs;

   } catch (BeanCreationException var6) {

       throw var6;

   } catch (Throwable var7) {

       throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", var7);

   }

}

 

빈의 의존관계 객체 정보를 만들고 이를 기반으로 @Autowired가 붙어있는 빈에 대한 의존주입을 진행한다.

 

허나 이미 의존관계가 설정되어있는 경우에는 AutowiredAnnotationBeanPostProcessor는 아무 처리하지 않는다.

 

case1) 생성자 주입

 

@RestController

class MemberController(

   private val testService: TestService,

   private val secondTestService: SecondTestService,

)

 

@Component

class TestService

 

@Component

class SecondTestService

 

위와 같이 생성자 주입으로만 이루어진 경우 빈의 속성을 보면

 

testService와 secondTestService 빈이 이미 존재한다.

MemberController의 생성자에는 testService와 secondTestService를 주입 받아야하는 생성자만 존재하므로

인스턴스화 될 때 의존관계가 모두 설정되어있다.

 

메타 정보(의존 관계 정보) 기반으로 의존 주입하는 metadata.inject(bean, beanName, pvs); 메서드 실행 이후에도
injectedElements 정보에 아무런 객체가 없다.
이미 의존관계가 설정되어있으니 주입할 빈 객체가 없는 것이다.

AutowiredAnnotationBeanPostProcessor가 사실상 아무 처리도 하지 않는다.

 

 

case2) 필드 주입

 

@RestController

class MemberController(

   private val testService: TestService,

) {

   @Autowired

   lateinit var secondTestService: TestService

}

 

빈 속성을 보면 testService는 실제 패키지 경로가 찍혀있지만, secondTestService의 경우 null이 찍힌다.

그리고 metadata.inject(bean, beanName, pvs); 를 실행하면 그 이후에 아래와 같이 injectElements에 secondTestService가 들어가 있는 것을 확인할 수 있다.

 

AutowiredAnnotationBeanPostProcessor는 @Autowired 어노테이션이 붙은 의존관계 객체를 주입해주는 것을 확인하였다.

 

 

2.3 AnnotationAwareAspectJAutoProxyCreator

@Aspect가 붙은 클래스에 부가기능을 적용한 프록시를 생성하여 이 프록시가 빈으로 등록되게 한다.

이를 통해 포인트컷이 적용된 타깃 클래스의 메서드를 실행하면 프록시의 메서드가 실행되므로 부가기능까지 함께 실행이 되는 것이다. 

 

AnnotationAwareAspectJAutoProxyCreator가 동작하는 과정을 알아보기 위해서는 

@Aspect가 적용된 타겟 클래스가 필요하다.

 

샘플 코드를 만들고 동작을 추적해보자.

 

@Aspect

@Component

class TestAspect {

   @Around("execution(* com.hexagonal.bootstrap.TestTarget.*(..))")

   fun proxyMethod(joinPoint: ProceedingJoinPoint): Any? {

       println("beforeMethod")

       val result = joinPoint.proceed() // 타겟 메서드 실행

       println("afterMethod")

       return result

   }

}

 

@Component

class TestTarget {

   fun targetMethod() {

       println("targetMethod")

   }

}

 

위와 같이 코드를 만들고 프로젝트를 구동하면 

스프링 컨텍스트가 모든 BeanPostProcessor를 가져와 실행시킨다.

 

AbstractAutowireCapableBeanFactory 클래스의 applyBeanPostProcessorsAfterInitialization() 메서드를 보면 

processor.postProcessAfterInitialization(result, beanName)

 

위와 같은 코드가 존재한다.

 

여기서 processor가 AnnotationAwareAspectJAutoProxyCreator 객체이다.

AnnotationAwareAspectJAutoProxyCreator의 postProcessAfterInitialization 메서드를 실행시킨다.

이 메서드는 AnnotationAwareAspectJAutoProxyCreator의 상위 타입 AbstractAutoProxyCreator에 존재한다.

 

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {

   if (bean != null) {

       Object cacheKey = this.getCacheKey(bean.getClass(), beanName);

       if (this.earlyBeanReferences.remove(cacheKey) != bean) {

           // 프록시 객체로 래핑이 필요로 프록시로 래핑하여 반환

           return this.wrapIfNecessary(bean, beanName, cacheKey);

       }

   }

 

   return bean;

}

 

메서드를 보면 프록시로 래핑하는 메서드가 존재한다.

다시 한번 이 메서드를 추적하면 아래와 같은 코드들이 존재한다.

 

Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));

 

타깃 객체에 대한 부가기능이 적용된 프록시를 반환함으로써

타깃 객체의 메서드가 실행될 때 부가 기능까지 실행될 수 있는 것이다.

@SpringBootTest

class TestClass(

   @Autowired private val context : ApplicationContext

) {

   @Test

   fun `프록시 객체 싱글톤레지스트리에 저장 확인`() {

       val testTarget =  context.getBean("testTarget")

       assertThat(AopUtils.isAopProxy(testTarget)).isTrue()

   }

}

위와 같은 테스트 코드를 돌려보자.

성공할 것이다.

 

이를 통해 실제 객체가 아닌 프록시 객체가 싱글톤 레지스트리에 저장되어 부가기능을 적용해주는 것을 알 수 있다.

 

반응형