[SpringBoot] @ComponentScan 동작 원리

2024. 12. 9. 23:00· Spring Framework/spring
목차
  1. 1. @ComponentScan의 역할
  2. 2. Spring 애플리케이션 초기화와 @ComponentScan
  3. 3. @ComponentScan 처리 흐름
  4. 3.1. @ComponentScan 메타데이터 읽기
  5. 3.2. 패키지 스캔
  6. 3.3. 빈 정의 등록
  7. 5. 정리:
반응형

@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은 다음과 같은 작업을 수행합니다:

  1. 패키지 스캔:
    • 지정된 패키지에서 @Component, @Service, @Controller, @Repository 등의 어노테이션이 붙은 클래스를 검색
  2. 빈 정의 등록:
    • 스캔된 클래스들을 BeanDefinitionRegistry에 빈 정의로 등록하여 Spring 컨테이너가 관리할 수 있도록 합니다.
  3. 유연한 구성 지원:
    • basePackages나 basePackageClasses를 사용해 스캔 대상 범위를 세부적으로 지정할 수 있습니다.

2. Spring 애플리케이션 초기화와 @ComponentScan

Spring 애플리케이션 초기화 과정에서 @ComponentScan은 @Configuration 클래스의 일부로 처리되는데요,
애플리케이션이 실행되면, Spring은 설정 클래스에서 @ComponentScan 정보를 읽고 패키지를 스캔하여 빈 정의를 등록하게 됩니다.

 

동작 순서

  1. 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();

        ...
    }

    ...
}
  1. ConfigurationClassPostProcessor 실행:
    • Spring은 @Configuration, @ComponentScan 등의 어노테이션을 처리하기 위해 ConfigurationClassPostProcessor를 사용합니다.
    • 이 클래스는 설정 클래스에서 어노테이션 메타데이터를 읽고, @ComponentScan을 기반으로 패키지를 스캔합니다.
// 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;
        }
    }
    ...

}
  1. 패키지 스캔 및 빈 정의 등록:
    • Spring은 ClassPathBeanDefinitionScanner를 통해 지정된 패키지를 스캔하고, 발견된 클래스를 빈 정의로 변환하여 BeanDefinitionRegistry에 등록합니다.
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의 내부 동작 원리를 살펴 보며 정리를 진행했는데, 어노테이션 하나의 동작이 방대한 내용을 담고있어서 그런지 추가적으로 설명이 필요한 부분이 아직 많이 보이는 것 같네요.

그동안에는 누군가의 지식을 수동적으로 접하기만 바빴었는데, 부족하지만 이렇게 스스로 분석하는 과정이 강의나 도서보다 더 큰 의미가 있는 좋은 경험이었습니다.

반응형
LIST

'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
  1. 1. @ComponentScan의 역할
  2. 2. Spring 애플리케이션 초기화와 @ComponentScan
  3. 3. @ComponentScan 처리 흐름
  4. 3.1. @ComponentScan 메타데이터 읽기
  5. 3.2. 패키지 스캔
  6. 3.3. 빈 정의 등록
  7. 5. 정리:
'Spring Framework/spring' 카테고리의 다른 글
  • [SpringBoot] Spring FIiter
  • [SpringBoot] ApplicationEvent를 활용한 이벤트 발행/구독
  • [SpringBoot] SpringBoot는 어떻게 실행될까?
  • MSA 환경에서 Kotlin + Spring Rest Docs + Swagger UI 적용
iron_jin
iron_jin
배운 것에 대한 내 생각을 가지고 정리하자
學而不思則罔(학이불사즉망)배운 것에 대한 내 생각을 가지고 정리하자
iron_jin
學而不思則罔(학이불사즉망)
iron_jin
전체
오늘
어제
  • 전체 (163)
    • 도서 (10)
    • 생각 정리 (0)
    • 후기 모음 (14)
    • 언어 (20)
      • css (1)
      • java (9)
      • Kotlin (0)
      • javascript (0)
      • Solidity (3)
      • Python (3)
      • GO (3)
      • C++ (1)
    • Spring Framework (32)
      • spring (16)
      • JPA (6)
      • Error (4)
      • Settings (4)
    • 알고리즘 (62)
      • 이론 (0)
      • 연습문제 (58)
    • 인프라 (6)
      • 클라우드 (1)
      • 도커 (0)
      • AWS (4)
      • Elastic Search (0)
    • 쿠버네티스 (3)
      • 이론 (0)
      • 실습 (2)
      • 트러블슈팅 (1)
    • Nginx (2)
    • CS (4)
      • 서버 (0)
      • 리눅스 (2)
      • 네트워크 (0)
      • OAuth (2)
    • 형상관리 (3)
      • GIT (3)
    • Open API (3)
      • 카카오 API (1)
      • QGIS (2)
    • 보안 (0)
      • 알고리즘 (0)
    • 공통 (1)
      • 성능 관리 도구 (1)
    • Database (2)
      • MySQL (1)
      • Redis (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 도메인 주도 개발
  • 프로그래머스
  • Java
  • 2019 카카오
  • 항해플러스
  • MySQL
  • 스프링 부트
  • 카카오 겨울 인턴십
  • 스프링
  • spring boot
  • Python
  • SpringBoot
  • 2019 카카오 블라인드
  • JPA
  • 코딩테스트
  • 알고리즘
  • ddd
  • 2020 kakao
  • 자바
  • 항해99
  • spring
  • Hibernate
  • 2020 카카오 블라인드
  • AWS
  • 2020 카카오
  • 2019 kakao
  • 백준
  • 2018 카카오 블라인드
  • 2018 kakao
  • 에릭 에반스

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
iron_jin
[SpringBoot] @ComponentScan 동작 원리
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.