처음으로 스프링 MVC로 프로젝트를 개발하다보니 모르는것도 많고 어떻게 해야하는지 정확히 잘 몰라 구글링과 스프링 설정에 대한 블로그들을 보면서 이것저것 복사 붙여넣기 식으로 수행했습니다.
그 중에서 스프링 XML 설정 지옥에서 벗어나게 해준 어노테이션. 스프링이 제공하는 어노테이션 기반 설정은 많지만 가장 유용했던 단 한 줄로 설정할 수 있었습니다.
Component Scan이 어떻게 내부적으로 수행하는지 살펴보겠습니다.
Component Scan
Component Scan은 XML에 일일이 빈등록을 하지않고 각 빈 클래스에 @Component를 통해 자동 빈 등록
@Component는 스프링이 어노테이션에 담긴 메타정보를 이용하기 시작했을 때 @Autowired와 함께 소개된 대표적인 어노테이션. @Component 어노테이션을 클래스에 작성하면 빈 스캐너를 통해 자동 빈 등록됩니다.
특정 패키지 아래에 위치한 빈들을 Component Scan하기 위한 방법은 다음과 같습니다.
자바 파일의 설정 클래스(@Configuration)에서 @ConponentScan 어노테이션 설정
Context Scan을 처리하는 부분은 spring-context-버전.jar에 위치하고 있으며 본문에 나오는 테스트코드와 소스는 스프링 프로젝트에서 확인하실 수 있습니다.
스프링 프로젝트 소스 에서 구할 수 있습니다.
분석은 크게 3가지 방향으로 진행하였습니다.
Component scan를 수행하는 과정.
컨테이너에서 getBean(beanName.class)을 호출하여 빈이 호출되는 과정.
Spring MVC에서 Component Scan
스프링 컨테이너가 실행되면서 수행하는 여러가지 초기화 과정 중에서 Component Scan부분만 딱 잘라서 설명할 경우 앞뒤 과정이 없어 생소할 수 있기 때문에 기본적인 빈 등록, 빈 검색을 같이 연관성있는 부분들도 같이 설명하도록 하겠습니다.
실질적인 Component scan이 진행되기까지 절차가 길기 때문에 요약을 하자면 아래와 같은 순서로 진행됩니다.
@Configuration 어노테이션 클래스 파싱 (보통 @Configuration 설정 클래스에 @ComponentScan이 설정)
@ComponentScan 어노테이션 클래스 파싱
ComponentScanAnnotationParser 클래스
실질적으로 Component scan 패키지 디렉토리에서 리소스를 찾는 ClassPathBeanDefinitionScanner
ComponentScan Test Code
ComponentScan에 대한 테스트 케이스는 아래와 같습니다. ComponentScan수행시 기본적으로는 base-package를 지정하지만 이 외에도 IncludeFilter, ExcludeFilter, ScopeProxy 등 다양항 설정 가능합니다.
여기서는 가장 기본적인 base-package를 기반으로 빈 등록하는 과정만 집중적으로 살펴보겠습니다.
5번째 줄의 AnnotationConfigApplicationContext 클래스는 스크링 컨테이너로 이름에서 유추할 수 있듯 어노테이션 설정을 처리할 수 있는 Context.
19~21번째 줄의 @Configuration(설정 어노테이션)과 @ComponentScan 어노테이션이 붙은 클래스(ComponentScanAnnotatedConfig_WithValueAttribute)를 등록(register)하고 이를 호출하는 테스트 코드입니다. “example.scannable” 패키지 위치에는 @Component 어노테이션을 가진 클래스 파일들이 위치해 있습니다.
Component scan을 수행하기 전에 register() 메서드로 검색할 패키지에 대한 정보를 가지는 @ComponentScan 어노테이션과 설정에 관련된 클래스임을 알려주는 @Configuration을 가진 ComponentScanAnnotatedConfig_WithValueAttribute.class를 빈 등록. (빈을 등록하는 절차 조금 복잡한 절차이지만 생략해도 Componentscan을 이해하는데 지장이 없으므로 분석을 건너뛰도록 하겠습니다.)
7번째 줄의 ctx.refresh() 메서드에 Component Scan 처리 부분이 담겨있습니다.
component scan 분석
AnnotationConfigApplicationContext.refresh()
AnnotationConfigApplicationContext의 빈 등록(register)을 마친 후 refresh() 메서드를 호출하는데 여기서 9번째 줄에서 빈 팩토리를 준비하고(prepareBeanFactory(beanFactory)) 이와 관련된 후처리(PostProcessor) 작업과 같은 몇몇 절차가 있으나 그 중 Component Scan은 19번째 줄의 invokeBeanFactoryPostProcessors(beanFactory) 메서드에서 이뤄집니다.
Component Scan 부분만 살펴보기 위해 나머지 메서드들에 대한 내용은 생략하겠습니다.
여기서 매개변수로 넘겨준 beanFactory는 DefaultListableBeanFactory 구체 클래스로 빈들을 등록/검색할 수 있는 BeanDefinitionRegistry 인터페이스를 상속하고 있습니다. 등록된 빈들은 DefaultListableBeanFactory 클래스의 ConcurrentHashMap타입의 beanDefinitionMap에 저장됩니다.
DefaultListableBeanFactory 팩토리에서 상속하는 인터페이스의 타입 계층 구조를 살펴보면 이름을 통해 어떤 역활을 해주는지 유추해 볼 수 있습니다.
DefaultListableBeanFactory 클래스는 빈을 싱글톤으로 관리해주는 SingletonRegistry 인터페이스 역시 상속하고 있습니다.
타입 계층 구조의 각 클래스에는 의존성이나 빈의 등록과 세세한 설정에 대한 사항을 다루는 부분이 담겨있습니다.
invokeBeanFactoryPostProcessors(beanFactory)에서 빈 팩토리를 BeanDefinitionRegistry로 형 변환 하여 넘기게 됩니다.(단순 형변환하여 넘기는게 아니라 부가 작업들이 있지만 중요한 내용이 아니므로 생략하였습니다.)
invokeBeanFactoryPostProcessors() 메서드 빈 등록을 처리하기 위해 여러 작업을 수행하지만 실질적으로 @Configuration 어노테이션 클래스를 파싱하는 것은 ConfigurationClassParser 클래스의 parse() 메서드입니다.
이 때 매개변수로 BeanDefinitionRegistry(beanFactory에 포함된 빈 registry)를 넘겨줍니다. 그리고 이 beanFactory에는 테스트 코드에서 컨텍스트에 등록해주었던 ComponentScanAnnotatedConfig_WithValueAttribute 클래스의 정보도 같이 포함되어 있습니다.
parse 메서드 안에서도 다시 몇 번의 메서드 호출 뒤 doProcessConfigurationClass 메서드로 넘어오며 여기서 실질적으로 Component Scan작업이 수행됩니다.
테스트 코드의 설정 클래스에서 @ComponentScan(“example.scannable”)로 설정했기 때문에 basePackages 값이 비어있고 기본값을 넣어주는 value에 “example.scannable” 값이 들어가 있는것을 확인할 수 있습니다. 하지만 내부에서 처리할 때에는 basePackages나 value에 넣어두나 동일한 과정을 거치게 됩니다.
sourceClass.getMetadata().getClassName()
SourceClass 클래스는 간단히 설명하자면 Configuration 정보가 있는 클래스의 인스턴스와 이 클래스의 어노테이션과 같은 메타정보를 분석해주는 AnnotationMetadata을 가지도록 Wrapping한 것이라 보시면 됩니다. 그렇기 때문에 sourceClass.getMetadata().getClassName()는 @Configuration 어노테이션이 있는 “org.springframework.context.annotation.ComponentScanAnnotatedConfig_WithValueAttribute” 가 됩니다.
위 메서드 두 번째 줄에 선언된 ClassPathBeanDefinitionScanner 클래스의 인스턴스는 이름에서 알 수 있듯 클래스패스에서 BeanDefinition을 찾는다.
위 코드의 생략된 부분은 첫 번째 매개변수인 componentScan에 포함되어있는 @ComponentScan의 각 옵션들(excludeFilters, scopeProxy, nameGenerator 등)을 읽어 이를 처리할 수 있도록 ClassPathBeanDefinitionScanner의 인스턴스 scanner에 설정하는 코드.
그리고 나머지 부분은 scan작업을 할 package, classes들을 설정하여 scanner.doScan() 메서드를 통해 스캔작업을 실행한다. 이 예제에서는 특정 패키지(example.scannable)를 스캔하게 된다.
실질적으로 Component scan 패키지 디렉토리에서 리소스를 찾는 ClassPathBeanDefinitionScanner
ClassPathBeanDefinitionScanner.doScan(String…)
여기서 5번째 줄의 Set candidates = findCandidateComponents(basePackage); 에서 특정 패키지에 속한 리소스들(파일들)을 모두 읽어온다.
6번째 줄에서 packageSearchPath에는 “classpath:example/scannable/**/.class” 와 같은 문자열로 설정되어 특정 클래스 패스의 특정 패키지 밑에 소속한 모든 .class파일들이 스캔 대상이 된다.
9 번째 줄의 this.resourcePatternResolver.getResources(packageSearchPath)을 통해 Resource들을(여기선 모두 파일이기 때문에 FileSystemReosource) 구할 수 있다. 즉, example.scannable 패키지에 위치한 모든 클래스 파일들을 구할 수 있다.(inner class포함)
메소드 실행 후 리소스들을 구했다면 다음에 실행되는 for문에서 각 리소스를 순회하는데 if문의 isCandidateComponent(metadataReader)를 통해 ComponentScan에 excludeFilter, includeFilter와 같은 필터를 설정해두었다면 이를 검사한다.(아래코드 참조)
위 코드에서 별도의 excludeFilter를 설정해두지 않았기 때문에 생략되지만 includeFilter에는 기본적으로 3가지 필터가 있는데 그 중 하나가 클래스의 어노테이션 중 org.springframework.stereotype.Component 어노테이션을 가졌는지 검사하는 필터가 있다. 즉 여기서 @Component 어노테이션을 가진 클래스들은 필터를 통과해 빈 등록 과정을 거치게 된다.
리소스들 중 필터를 거친 리소스들을 읽어와 그다음으로 수행하는 for문에서 빈이 싱글톤으로 관리될지 아니면 새로운 인스턴스를 만드는지를 판별하는 Scoped Proxy와 beanNameGenerator를 통해 bean의 이름을 생성한 뒤 이를 BeanDefinitionHolder로 만들어 beanDefinitions(LinkedHashSet)에 담아두고 이를 registry에서 등록하게 됩니다.
여기서 registry는 제일 처음 매개변수로 넘겨주었던 beanFactory로 DefaultListableBeanFactory 클래스의 인스턴스입니다.
결론적으로 빈 팩토리의 beanResgitry에 등록된다.
빈 호출과정
Component scan으로 빈이 등록되었고, 테스트 코드의 9번째 줄에서 ctx.getBean(TestBean.class);를 처리하는 과정을 살펴보겠습니다.
빈을 등록했을때 beanFactory를 활용했듯이 역시 beanFactory에서 getBean(Class) 메서드로 찾고자 하는 빈의 클래스정보를 넘겨주면 beanFactory(DefaultListableBeanFactory)의 상위 타입인 SingletonBeanRegistry의 getSingleton(beanName)을 호출하게됩니다.
2번째 줄의 this.singletonObjects.get(beanName); 을통해 실질적인 TestBean 빈을 찾게되는데 singletonObjects는 이전 빈 등록과정에서 registry 처럼 ConcurrentHashMap<String, Object>(64) 타입이다.
Component Scan시 빈이 등록되는 곳은 DefaultListableBeanFactory클래스의 beanDefinitionMap 이라고 언급했는데 Component Scan을 진행하는 메서드 stack 중 ConfigurationClassPostProcessor.processConfigBeanDefinitions() 메서드(본 글의 3번째 코드를 참조) 진행 후
위 코드에서 singletonRegistry가 BeanFactory인 DefaultListableBeanFactory.
singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); 는 singletonRegistry의 상위 클래스인 DefaultSingletonBeanRegistry에 자신의 registry에 등록된 빈들을 singleton일 경우 넘겨준다.
DefaultListableBeanFactory.registerSingleton()
지금까지가 Singleton 빈이 호출되는 과정이다.
지금까지 설명한 빈 검색은 싱글톤이 아닐경우나 다른 설정들이 있을 경우 다른 방식으로 진행되지만 기본적인 싱글톤 빈의 경우의 검색의 경우에 해당한다.
Spring MVC에서의 Component Scan
위 분석에서는 AnnotationConfigApplicationContext 클래스가 스프링 컨테이너로 사용되었지만 우리가 실질적으로 사용하는 Spring MVC에서 사용되는 Context는 org.springframework.web.context.support.XmlWebApplicationContext 다.
이는 spring-web-버전.jar에서 org.springframework.web.context 패키지에 ContextLoader.properties 파일을 열어보면 알 수 있다.
ContextLoader.properties
XmlWebAppplicationContext에서는 초기화 과정에서 상위 클래스인 AbstractRefreshableApplicationContext.refreshBeanFactory() 메서드를 통해 DefaultListableBeanFactory를 생성하여 사용한다.
이후의 과정은 생략된 부분이 많이 있지만 Component Scan은 앞서 설명한 과정을 통해 빈을 등록하게 된다.
부록 1. @Component
@Component는 스프링이 어노테이션에 담긴 메타정보를 이용하기 시작했을 때 @Autowired와 함께 소개된 대표적인 어노테이션입니다. @Component는 클래스에 부여되는데 이는 빈 스캐너를 통해 자동으로 빈으로 등록됩니다. 정확히는 @Component 또는 @Component를 메타 어노테이션으로 갖고 있는 어노테이션(@Controller, @Repository, @Service를 말함)이 붙은 클래스가 빈 등록 대상이 됩니다.
@Component의 @Controller, @Repository, @Service
보통 스프링 MVC 코드를 작성할 때 @Controller, @Repository, @Service를 각 클래스의 성격에 맞추어 붙여주곤 했습니다.
가끔 @Component를 붙여도 별다른 차이 없이 동작하는걸 확인 할 수 있는데 이는 @Component 어노테이션이 앞서 언급한 3개의 어노테이션의 메타 어노테이션(어노테이션에 대한 어노테이션)으로 사용되었기 때문입니다.
스프링 컨테이너가 초기화 될 때 설정에따라(XML이나 어노테이션 기반 설정) 빈 등록과정에서 특정 클래스의 어노테이션을 읽어(리플렉션 API인 getDeclaredAnnoations(), getAnnotations()를 활용)들입니다. 이 때 해당 특정 어노테이션을 만나면 해당 어노테이션의 메타 어노테이션을 재귀적으로 읽어들이기 때문에 @Controller, @Repository, @Service를 만나면 그 메타 어노테이션인 @Component 어노테이션을 읽어들이게 되고 하나의 빈으로 등록하게 됩니다.
이 어노테이션들의 패키지명이 org.springframework.stereotype인데 왜 org.springframework.annotation이라는 패키지가 아닌 stereotype인가 의문이긴 한데 streotype를 사전에서 찾아보면 ‘정형화 하다, 형식화 하다’ 라는 의미가 있어 그 의미에 맞추어 패키지명을 정하지 않았는가 짐작해봅니다.
네 개의 어노테이션 말고도 @Component를 메타 어노테이션으로 가진 사용자 정의 어노테이션도 정의 가능합니다. 어노테이션의 용도가 AOP에서 포인트컷을 작성할 경우와 같이 다양한 용도로 활용할 수 있습니다.
일반적으로 @Component와 나머지 세 개의 어노테이션이 차이점이 없음에도 불구하고 각 클래스의 성격에 맞추어 어노테이션을 붙이길 권장하는데 이는 각 클래스의 성격에 맞게 작성하는게 가독성이나 적합한 의미부여에 좋습니다.
@Controller의 코드를 확인하면 @Conponent 어노테이션을 어노테이션으로 가지고 있습니다.