Enjoy My Posts

Spring Boot가 annotation을 활용하여 빈을 등록하는 과정

Posted on By Geunwon Lim

서론

스프링을 공부하면서 IOC, PSA, AOP 등 핵심 개념은 어느정도 이해했지만, 어떻게 보면 가장 근간이 되는 빈 등록을 어떻게 하는지 모르고 있었습니다.

xml을 쓰던, 어노테이션을 쓰던 어떤 방식이든 빈으로 등록되는 것이 중요하다고 생각했거든요. 그리고 저는 어노테이션을 통한 빈 등록이 주류가 된 이후에 스프링 공부를 시작했기 때문에 xml을 활용하는 방식은 그다지 관심이 없었고, 어노테이션으로 등록하는 방법에 대해 대강 ‘리플렉션으로 @ComponentScan 붙은 애들 불러오나보다’라고 추측하고 있었습니다.

그러던 중 문득 어떻게 컴포넌트 스캔을 하는지 궁금해져서 스프링 부팅 시 빈 등록 과정을 따라가봤습니다. 제가 추측한 내용이 아예 틀린 말은 아닐지 몰라도, 실제로는 제가 추측하던 방식과 조금 달랐습니다. 어노테이션이 붙은 애들을 가져오는 게 아니라, 파일 기반으로 루트 패키지 밑의 모든 클래스들을 가져오고, @Component 어노테이션이 붙은 클래스들만 걸러내는 식이었습니다.

본론

가장 궁금한 것은 빈을 등록하는 방식이었지만, 평소 궁금한 것들도 차근차근 공부해봤습니다.

  1. 난 아무것도 설정해준 것이 없는데 왜 xml이 아닌 어노테이션 기반으로 빈을 등록할까?‘가 궁금한 것 중 하나였습니다.

    그것은 그냥 스프링부트 기본 설정이 어노테이션 기반 빈 등록이기 때문입니다.

    public class SpringApplication {
       
       /**
        * The class name of application context that will be used by default for non-web
        * environments.
        */
       public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
             + "annotation.AnnotationConfigApplicationContext";
         
      ...
    

    스프링 부트 패키지에 있는 SpringApplication을 보면 기본 ApplicationContext가 어노테이션 기반 컨텍스트로 설정이 돼있습니다.

    스프링부트 어플리케이션을 run 하면 일단 컨텍스트를 생성하는데, 이 때 contextClass가 어노테이션 기반 Context로 설정이 됩니다.

    protected ConfigurableApplicationContext createApplicationContext() {
       Class<?> contextClass = this.applicationContextClass;
       if (contextClass == null) {
          try {
             switch (this.webApplicationType) {
             case SERVLET:
                contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                break;
             case REACTIVE:
                contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                break;
             default:
                contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
             }
          }
         ...
    

    그리고 어노테이션 기반 Context는 기본적으로 스캐닝을 할 때 클래스 위치 기반 스캐너를 활용합니다.

    public AnnotationConfigApplicationContext() {
       this.reader = new AnnotatedBeanDefinitionReader(this);
       this.scanner = new ClassPathBeanDefinitionScanner(this);
    }
    

    이후 제가 잘 모르는 스프링의 기본(Config) 빈들이 등록이 되고나서, 다시 ApplicationContext를 생성하는데요. 이 때는 webApplicaionType이 “SERVLET”으로 설정되어 ApplicationContext가 AnnotationConfigServletWebServerApplicationContext가 생성됩니다. 어찌됐던 간에 기본 설정이 어노테이션 기반 컨텍스트인거죠.

    AnnotationConfigApplicationContext과 관련하여서 조금 틀린 부분이 있는 것 같아 수정하겠습니다…!

    생성되고 나면 registerDefaultFilters()가 호출되는데, 이 때 @Component 어노테이션이 includeFilter에 포함됩니다.

    protected void registerDefaultFilters() {
       this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    

    그리고

    protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
       String rootDirPath = determineRootDir(locationPattern);
    

    root 경로(저의 경우 classpath*:beyondeyesight/chat/)에 따라 이 폴더 밑에 있는 클래스들을 다 찾아냅니다(doFindMatchingFileSystemResources). 그리고 그 모든 클래스들 중 빈이 될 녀석들을 찾는 과정에서 후보군이 될 만한가 검사합니다(isCandidateComponent).

    protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
       for (TypeFilter tf : this.excludeFilters) {
          if (tf.match(metadataReader, getMetadataReaderFactory())) {
             return false;
          }
       }
       for (TypeFilter tf : this.includeFilters) {
          if (tf.match(metadataReader, getMetadataReaderFactory())) {
             return isConditionMatch(metadataReader);
          }
       }
       return false;
    }
    

    이 때 아까전에 @Component가 들어있는 includeFilters를 활용하여 클래스에 @Component 어노테이션이 붙어있는지 검사하죠.

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
          throws IOException {
       
       // This method optimizes avoiding unnecessary creation of ClassReaders
       // as well as visiting over those readers.
       if (matchSelf(metadataReader)) {
          return true;
       }
      ...
    
    @Override
    protected boolean matchSelf(MetadataReader metadataReader) {
       AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
       return metadata.hasAnnotation(this.annotationType.getName()) ||
             (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
    }
    
    default boolean hasAnnotation(String annotationName) {
       return getAnnotations().isDirectlyPresent(annotationName);
    }
    

결론

classpath 밑에 존재하는 모든 클래스들을 부른 다음, @Component 어노테이션이 붙은 클래스들만 걸러낸다.

후기

추측하던 것과 많이 다르진 않았지만, 기존에 추측만 하던 것과 비교하여 좀 더 구체화된 것 같습니다. 나중에 스프링같은 프레임워크를 만들 수 있다면 좋겠네요.