서론
스프링을 공부하면서 IOC, PSA, AOP 등 핵심 개념은 어느정도 이해했지만, 어떻게 보면 가장 근간이 되는 빈 등록을 어떻게 하는지 모르고 있었습니다.
xml을 쓰던, 어노테이션을 쓰던 어떤 방식이든 빈으로 등록되는 것이 중요하다고 생각했거든요. 그리고 저는 어노테이션을 통한 빈 등록이 주류가 된 이후에 스프링 공부를 시작했기 때문에 xml을 활용하는 방식은 그다지 관심이 없었고, 어노테이션으로 등록하는 방법에 대해 대강 ‘리플렉션으로 @ComponentScan 붙은 애들 불러오나보다’라고 추측하고 있었습니다.
그러던 중 문득 어떻게 컴포넌트 스캔을 하는지 궁금해져서 스프링 부팅 시 빈 등록 과정을 따라가봤습니다. 제가 추측한 내용이 아예 틀린 말은 아닐지 몰라도, 실제로는 제가 추측하던 방식과 조금 달랐습니다. 어노테이션이 붙은 애들을 가져오는 게 아니라, 파일 기반으로 루트 패키지 밑의 모든 클래스들을 가져오고, @Component 어노테이션이 붙은 클래스들만 걸러내는 식이었습니다.
본론
가장 궁금한 것은 빈을 등록하는 방식이었지만, 평소 궁금한 것들도 차근차근 공부해봤습니다.
-
‘난 아무것도 설정해준 것이 없는데 왜 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 어노테이션이 붙은 클래스들만 걸러낸다.
후기
추측하던 것과 많이 다르진 않았지만, 기존에 추측만 하던 것과 비교하여 좀 더 구체화된 것 같습니다. 나중에 스프링같은 프레임워크를 만들 수 있다면 좋겠네요.