
@ComponentScan의 동작 원리
Spring 프레임워크에서 @ComponentScan
은 지정된 패키지를 스캔하여 Spring 컨테이너에서 관리할 빈(Component)을 자동으로 등록하는 역할을 합니다.
이 어노테이션은 주로 @Configuration
클래스와 함께 사용되며, Spring 애플리케이션 초기화 과정에서 매우 중요한 부분을 차지합니다.
@ComponentScan은 @SpringBootApplication을 통해서 관리됩니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan( // 선언부
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}
1. @ComponentScan의 역할
@ComponentScan
은 다음과 같은 작업을 수행합니다:
- 패키지 스캔:
- 지정된 패키지에서
@Component
,@Service
,@Controller
,@Repository
등의 어노테이션이 붙은 클래스를 검색
- 지정된 패키지에서
- 빈 정의 등록:
- 스캔된 클래스들을
BeanDefinitionRegistry
에 빈 정의로 등록하여 Spring 컨테이너가 관리할 수 있도록 합니다.
- 스캔된 클래스들을
- 유연한 구성 지원:
basePackages
나basePackageClasses
를 사용해 스캔 대상 범위를 세부적으로 지정할 수 있습니다.
2. Spring 애플리케이션 초기화와 @ComponentScan
Spring 애플리케이션 초기화 과정에서 @ComponentScan
은 @Configuration
클래스의 일부로 처리되는데요,
애플리케이션이 실행되면, Spring은 설정 클래스에서 @ComponentScan
정보를 읽고 패키지를 스캔하여 빈 정의를 등록하게 됩니다.
동작 순서
SpringApplication.run()
호출:- 애플리케이션 실행 시 설정 클래스(
@SpringBootApplication
포함)가BeanDefinitionRegistry
에 등록됩니다.
- 애플리케이션 실행 시 설정 클래스(
// SpringApplication.class
public ConfigurableApplicationContext run(String... args) {
...
try {
...
context = this.createApplicationContext(); // 어노테이션 정보가 BeanRegistry에 등록되는 부분
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
startup.started();
...
}
...
}
ConfigurationClassPostProcessor
실행:- Spring은
@Configuration
,@ComponentScan
등의 어노테이션을 처리하기 위해ConfigurationClassPostProcessor
를 사용합니다. - 이 클래스는 설정 클래스에서 어노테이션 메타데이터를 읽고,
@ComponentScan
을 기반으로 패키지를 스캔합니다.
- Spring은
// ConfigurationClassPostProcessor.class 내부
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
...
ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry);
do {
...
if (registry.getBeanDefinitionCount() > candidateNames.length) {
...
for(int var15 = 0; var15 < var28; ++var15) {
String candidateName = var27[var15];
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && !alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
} while(!candidates.isEmpty());
...
}
// @Configuration과 그 외의 어노테이션을 분리하여 BeanRegistry에 저장하는 클래스
public abstract class ConfigurationClassUtils {
...
private static final Set<String> candidateIndicators = Set.of(Component.class.getName(), ComponentScan.class.getName(), Import.class.getName(), ImportResource.class.getName());
static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
...
Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, "full"); // @Configuration Bean
} else {
if (config == null && !Boolean.TRUE.equals(beanDef.getAttribute(CANDIDATE_ATTRIBUTE)) && !isConfigurationCandidate(metadata)) {
return false;
}
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, "lite");// @ComponentScan, @Import 등등
}
Integer order = getOrder(metadata);
if (order != null) {
beanDef.setAttribute(ORDER_ATTRIBUTE, order);
}
return true;
...
}
// 해당 로직 내부에서 @Configuration과 그 외의 어노테이션을 분리하여 저장한다.
static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
if (metadata.isInterface()) {
return false;
} else {
Iterator var1 = candidateIndicators.iterator();
String indicator;
do {
if (!var1.hasNext()) {
return hasBeanMethods(metadata);
}
indicator = (String)var1.next();
} while(!metadata.isAnnotated(indicator));
return true;
}
}
...
}
- 패키지 스캔 및 빈 정의 등록:
- Spring은
ClassPathBeanDefinitionScanner
를 통해 지정된 패키지를 스캔하고, 발견된 클래스를 빈 정의로 변환하여BeanDefinitionRegistry
에 등록합니다.
- Spring은
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext implements AnnotationConfigRegistry {
private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;
private final Set<Class<?>> annotatedClasses;
private String[] basePackages;
public AnnotationConfigServletWebServerApplicationContext() {
this.annotatedClasses = new LinkedHashSet();
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
...
}
3. @ComponentScan 처리 흐름
3.1. @ComponentScan 메타데이터 읽기
Spring은 설정 클래스에서 @ComponentScan
을 찾고, 해당 어노테이션의 속성(basePackages
, excludeFilters
등)을 읽어옵니다. 이 정보는 Spring의 초기화 단계에서 사용됩니다.
3.2. 패키지 스캔
패키지 스캔은 Spring의 ClassPathBeanDefinitionScanner
에 의해 수행됩니다. 이 과정에서 다음 작업이 이루어지게 됩니다.
- 대상 패키지 확인:
basePackages
속성에 정의된 패키지를 스캔 대상으로 설정합니다.- 패키지가 명시되지 않은 경우, 설정 클래스가 위치한 패키지를 기본값으로 사용합니다.
- 컴포넌트 후보 탐색:
- 대상 패키지 내에서
@Component
,@Service
,@Controller
와 같은 어노테이션이 붙은 클래스를 탐색합니다.
- 대상 패키지 내에서
3.3. 빈 정의 등록
스캔된 클래스들은 BeanDefinition
으로 변환되어 BeanDefinitionRegistry
에 등록됩니다. 이 과정에서 각 클래스는 고유한 빈 이름을 생성하며, 빈 이름은 클래스명 또는 어노테이션의 이름 속성에 따라 결정됩니다.
5. 정리:
@ComponenScan의 내부 동작 원리를 살펴 보며 정리를 진행했는데, 어노테이션 하나의 동작이 방대한 내용을 담고있어서 그런지 추가적으로 설명이 필요한 부분이 아직 많이 보이는 것 같네요.
그동안에는 누군가의 지식을 수동적으로 접하기만 바빴었는데, 부족하지만 이렇게 스스로 분석하는 과정이 강의나 도서보다 더 큰 의미가 있는 좋은 경험이었습니다.
'Spring Framework > spring' 카테고리의 다른 글
[SpringBoot] Spring FIiter (0) | 2024.12.29 |
---|---|
[SpringBoot] ApplicationEvent를 활용한 이벤트 발행/구독 (0) | 2024.12.28 |
[SpringBoot] SpringBoot는 어떻게 실행될까? (0) | 2024.12.08 |
MSA 환경에서 Kotlin + Spring Rest Docs + Swagger UI 적용 (0) | 2023.09.14 |
Spring 컨트롤러 테스트 + Rest Docs에서 시간 개선 (0) | 2023.09.13 |

@ComponentScan의 동작 원리
Spring 프레임워크에서 @ComponentScan
은 지정된 패키지를 스캔하여 Spring 컨테이너에서 관리할 빈(Component)을 자동으로 등록하는 역할을 합니다.
이 어노테이션은 주로 @Configuration
클래스와 함께 사용되며, Spring 애플리케이션 초기화 과정에서 매우 중요한 부분을 차지합니다.
@ComponentScan은 @SpringBootApplication을 통해서 관리됩니다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan( // 선언부
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}
1. @ComponentScan의 역할
@ComponentScan
은 다음과 같은 작업을 수행합니다:
- 패키지 스캔:
- 지정된 패키지에서
@Component
,@Service
,@Controller
,@Repository
등의 어노테이션이 붙은 클래스를 검색
- 지정된 패키지에서
- 빈 정의 등록:
- 스캔된 클래스들을
BeanDefinitionRegistry
에 빈 정의로 등록하여 Spring 컨테이너가 관리할 수 있도록 합니다.
- 스캔된 클래스들을
- 유연한 구성 지원:
basePackages
나basePackageClasses
를 사용해 스캔 대상 범위를 세부적으로 지정할 수 있습니다.
2. Spring 애플리케이션 초기화와 @ComponentScan
Spring 애플리케이션 초기화 과정에서 @ComponentScan
은 @Configuration
클래스의 일부로 처리되는데요,
애플리케이션이 실행되면, Spring은 설정 클래스에서 @ComponentScan
정보를 읽고 패키지를 스캔하여 빈 정의를 등록하게 됩니다.
동작 순서
SpringApplication.run()
호출:- 애플리케이션 실행 시 설정 클래스(
@SpringBootApplication
포함)가BeanDefinitionRegistry
에 등록됩니다.
- 애플리케이션 실행 시 설정 클래스(
// SpringApplication.class
public ConfigurableApplicationContext run(String... args) {
...
try {
...
context = this.createApplicationContext(); // 어노테이션 정보가 BeanRegistry에 등록되는 부분
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
startup.started();
...
}
...
}
ConfigurationClassPostProcessor
실행:- Spring은
@Configuration
,@ComponentScan
등의 어노테이션을 처리하기 위해ConfigurationClassPostProcessor
를 사용합니다. - 이 클래스는 설정 클래스에서 어노테이션 메타데이터를 읽고,
@ComponentScan
을 기반으로 패키지를 스캔합니다.
- Spring은
// ConfigurationClassPostProcessor.class 내부
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
...
ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry);
do {
...
if (registry.getBeanDefinitionCount() > candidateNames.length) {
...
for(int var15 = 0; var15 < var28; ++var15) {
String candidateName = var27[var15];
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && !alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
} while(!candidates.isEmpty());
...
}
// @Configuration과 그 외의 어노테이션을 분리하여 BeanRegistry에 저장하는 클래스
public abstract class ConfigurationClassUtils {
...
private static final Set<String> candidateIndicators = Set.of(Component.class.getName(), ComponentScan.class.getName(), Import.class.getName(), ImportResource.class.getName());
static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
...
Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, "full"); // @Configuration Bean
} else {
if (config == null && !Boolean.TRUE.equals(beanDef.getAttribute(CANDIDATE_ATTRIBUTE)) && !isConfigurationCandidate(metadata)) {
return false;
}
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, "lite");// @ComponentScan, @Import 등등
}
Integer order = getOrder(metadata);
if (order != null) {
beanDef.setAttribute(ORDER_ATTRIBUTE, order);
}
return true;
...
}
// 해당 로직 내부에서 @Configuration과 그 외의 어노테이션을 분리하여 저장한다.
static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
if (metadata.isInterface()) {
return false;
} else {
Iterator var1 = candidateIndicators.iterator();
String indicator;
do {
if (!var1.hasNext()) {
return hasBeanMethods(metadata);
}
indicator = (String)var1.next();
} while(!metadata.isAnnotated(indicator));
return true;
}
}
...
}
- 패키지 스캔 및 빈 정의 등록:
- Spring은
ClassPathBeanDefinitionScanner
를 통해 지정된 패키지를 스캔하고, 발견된 클래스를 빈 정의로 변환하여BeanDefinitionRegistry
에 등록합니다.
- Spring은
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext implements AnnotationConfigRegistry {
private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;
private final Set<Class<?>> annotatedClasses;
private String[] basePackages;
public AnnotationConfigServletWebServerApplicationContext() {
this.annotatedClasses = new LinkedHashSet();
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
...
}
3. @ComponentScan 처리 흐름
3.1. @ComponentScan 메타데이터 읽기
Spring은 설정 클래스에서 @ComponentScan
을 찾고, 해당 어노테이션의 속성(basePackages
, excludeFilters
등)을 읽어옵니다. 이 정보는 Spring의 초기화 단계에서 사용됩니다.
3.2. 패키지 스캔
패키지 스캔은 Spring의 ClassPathBeanDefinitionScanner
에 의해 수행됩니다. 이 과정에서 다음 작업이 이루어지게 됩니다.
- 대상 패키지 확인:
basePackages
속성에 정의된 패키지를 스캔 대상으로 설정합니다.- 패키지가 명시되지 않은 경우, 설정 클래스가 위치한 패키지를 기본값으로 사용합니다.
- 컴포넌트 후보 탐색:
- 대상 패키지 내에서
@Component
,@Service
,@Controller
와 같은 어노테이션이 붙은 클래스를 탐색합니다.
- 대상 패키지 내에서
3.3. 빈 정의 등록
스캔된 클래스들은 BeanDefinition
으로 변환되어 BeanDefinitionRegistry
에 등록됩니다. 이 과정에서 각 클래스는 고유한 빈 이름을 생성하며, 빈 이름은 클래스명 또는 어노테이션의 이름 속성에 따라 결정됩니다.
5. 정리:
@ComponenScan의 내부 동작 원리를 살펴 보며 정리를 진행했는데, 어노테이션 하나의 동작이 방대한 내용을 담고있어서 그런지 추가적으로 설명이 필요한 부분이 아직 많이 보이는 것 같네요.
그동안에는 누군가의 지식을 수동적으로 접하기만 바빴었는데, 부족하지만 이렇게 스스로 분석하는 과정이 강의나 도서보다 더 큰 의미가 있는 좋은 경험이었습니다.
'Spring Framework > spring' 카테고리의 다른 글
[SpringBoot] Spring FIiter (0) | 2024.12.29 |
---|---|
[SpringBoot] ApplicationEvent를 활용한 이벤트 발행/구독 (0) | 2024.12.28 |
[SpringBoot] SpringBoot는 어떻게 실행될까? (0) | 2024.12.08 |
MSA 환경에서 Kotlin + Spring Rest Docs + Swagger UI 적용 (0) | 2023.09.14 |
Spring 컨트롤러 테스트 + Rest Docs에서 시간 개선 (0) | 2023.09.13 |