이번 포스트에서는 내가 궁금해서 알아본 생성자 주입이 이루어지는 과정에 대해서 설명할 것이다.
📌 서론
이번 포스트는 총 2편으로 나뉜다. 지금부터 설명할 1편에서는 @ComponentScan이 빈 등록을 어떻게 하는지 알아보고 2편에서는 이 등록된 빈을 활용하여 생성자 주입을 진행하는 것을 상세히 알아본다. 이번 포스트의 내용은 스프링에서 가장 중요하게 사용되는 Bean을 등록하는 과정을 상세하게 설명하도록 하겠다.
1. 생성자 주입과 컴포넌트 스캔 간략하게 알아보기
1-1 생성자 주입 과정 알아보기
1. SpringApplication.run()
- 스프링 부트 애플리케이션을 시작하는 주요 진입점이다. 이 메서드가 호출되면 내부적으로 여러 설정과 초기화 작업이 진행된다.
2. ApplicationContext 생성
- SpringApplication.run()은 스프링 애플리케이션 컨텍스트(ApplicationContext)를 생성하고 초기화한다. 이 컨텍스트는 스프링 빈(bean)의 정의, 생성 및 관리를 담당한다.
3. @SpringBootApplication
- 이 어노테이션은 스프링 부트의 핵심 어노테이션이다. @SpringBootApplication은 자동 구성(@EnableAutoConfiguration), 구성 스캔(@ComponentScan), 그리고 여러 설정(@Configuration)을 포함하고 있다.
4. @ComponentScan
- 이 어노테이션을 통해 스프링은 지정된 패키지 내에서 컴포넌트(@Component, @Service, @Repository, @Controller 등)를 스캔하고 빈으로 등록한다.
5. ClassPathBeanDefinitionScanner
- 이 클래스는 클래스 패스를 스캔하여 빈 정의를 찾고 등록하는 역할을 한다. 각각의 빈 정의(BeanDefinition)는 스프링 컨테이너가 관리하는 빈의 메타데이터를 담고 있다.
6. BeanFactory에서 생성자 찾기
- 스프링은 빈을 생성하기 전에 빈의 생성자를 결정해야 한다. 이 과정에서 빈의 생성자와 빈에 주입할 의존성들을 찾아낸다. 실제로 이 과정은 BeanFactory의 구현체인 DefaultListableBeanFactory에서 주로 이루어 진다.
7. 생성자를 통한 빈 인스턴스 생성 및 의존성 주입
- 스프링에서 생성자를 통한 빈 인스턴스 생성 및 의존성 주입 과정은 주로 AbstractAutowireCapableBeanFactory 클래스에서 처리된다. 이 클래스는 DefaultListableBeanFactory에 의해 사용되며, 빈의 생성과 의존성 주입, 초기화 등을 관리한다.
- autowireConstructor 메서드를 통해 생성자 주입을 수행한다. 빈의 생성자를 찾고, 필요한 의존성들을 ConstructorResolver 클래스에 선언된 resolveConstructorArguments 메서드를 통해 해결한 후, 이들을 생성자를 통해 주입하면서 빈 인스턴스를 생성한다.
8. 생성자 주입 완료
- 이 단계에서 빈의 생성 및 의존성 주입이 완료된다. 이제 애플리케이션에서 사용할 준비가 되었다.
컴포넌트 스캔(@ComponentScan)은 어떻게 진행되는지 알아보자
1-2. 컴포넌트 스캔(@ComponentScan) 과정 알아보기
1. 스프링 부트 실행
- 스프링 부트를 실행하면 @SpringBootApplication 어노테이션이 동작한다.
- @SpringBootApplication어노테이션 내부에는 아래와 같이 @ComponentScan이 적혀져 있고 이 어노테이션을 통해 컴포넌트 스캔이 진행된다. 이 과정에서 ClassPathBeanDefinitionScanner 클래스가 중요한 역할을 한다.
2. ClassPathBeanDefinitionScanner 클래스의 동작
- ClassPathBeanDefinitionScanner 클래스가 프로젝트의 classPath를 스캔하면서 빈으로 등록할 클래스들을 찾는다.
3. scan 메서드
- ClassPathBeanDefinitionScanner 클래스 내부에 있는 scan 메서드는 지정된 패키지 내의 클래스를 스캔하고, 이를 BeanDefinition으로 변환해서 레지스트리에 등록한다. scan 메서드는 스캔 시작 전 후의 BeanDefinition 개수의 차이를 반환하면서 스캔이 몇 개의 빈을 발견했는지 알려준다.
4. doScan 메서드
- scan메서드가 내부에서 호출한 doScan 메서드는 실제로 패키지를 스캔하고, 조건에 맞는 컴포넌트를 찾아서 BeanDefinition으로 변환하는 역할을 한다. 이 메서드는 BeanDefinitionHolder의 집합을 반환하고, 각 BeanDefinition에 필요한 메타데이터를 설정한다.
5. doScan 메서드 내부의 처리과정
- 지정된 패키지 내의 후보 클래스를 찾는다.
- 각 후보 클래스에 대해 빈의 범위를 결정하고, 빈 이름을 생성한다.
- BeanDefinition를 후처리하고, 필요한 주석을 처리한다.
- BeanDefinition의 적합성을 검사한 후, 레지스트리에 등록한다.
6. 이후의 작업
- 스캔이 완료되고, BeanDefinitionHolder들이 레지스트리에 등록되면, 스프링 컨테이너는 이 BeanDefinition들을 사용해서 실제 빈 인스턴스를 생성하고 관리한다. 이렇게 생성된 빈들은 스프링 애플리케이션 컨텍스트에서 관리되며, 의존성 주입, 라이프사이클 관리 등 스프링이 제공하는 여러 기능을 활용할 수 있게 되는것이다.
컴포넌트 스캔을 할 때 스프링은 내부적으로 ClassPathBeanDefinitionScanner 클래스를 사용한다.
2. 컴포넌트 스캔 - ClassPathBeanDefinitionScanner 클래스
2-1. ClassPathBeanDefinitionScanner란 무엇인가?
- ClassPathBeanDefinitionScanner는 스프링 프레임워크의 핵심 컴포넌트로, 클래스 경로 내의 컴포넌트를 스캔하고 이를 BeanDefinition으로 변환하는 기능을 수행한다.
ClassPathBeanDefinitionScanner는 주어진 패키지 경로 내에서 어노테이션 기반 컴포넌트를 식별하고, 각 컴포넌트에 대한 메타데이터를 수집하여 BeanDefinition 객체로 만든다. 이 객체는 스프링 컨테이너가 해당 빈을 생성하고 관리하는 데 필요한 모든 정보를 포함한다.
2-2. 컴포넌트 스캔 및 스캔된 각 클래스에 대한 BeanDefinition 생성
- 스프링 부트가 시작되면, @ComponentScan 어노테이션에 지정된 패키지 내에서 컴포넌트들을 스캔하는 과정이 진행된다. 이 과정은 ClassPathBeanDefinitionScanner 클래스에 의해서 수행된다. @ComponentScan은 스프링에게 어떤 패키지를 스캔할지 지시하는 역할을 하며, 실제 스캐닝 작업은 ClassPathBeanDefinitionScanner이 실행한다.
- 스캔 과정에서 @RestController, @Service, @Repository, @Component 등의 어노테이션이 붙은 클래스들이 식별된다. 이렇게 식별된 클래스들에 대해서 ClassPathBeanDefinitionScanner는 각 클래스에 해당하는 BeanDefinition을 생성한다.
- 이렇게 생성된 BeanDefinition은 클래스에 대한 중요한 정보를 담고 있다. 이 정보에는 클래스의 타입, 스코프, 라이프사이클 콜백 메서드 등이 포함되어 있으며, 스프링이 이 클래스를 어떻게 생성하고 관리해야 할지에 대한 지침을 제공한다.
@ComponentScan으로 등록된 빈은 생성자 주입에 사용된다. 그래서 스프링이 생성자 주입을 하는 전체 과정을 이해하기 위해서는 가장 먼저 선행되는 작업인 @ComponentScan으로 빈을 등록하는 과정부터 알아야 한다.
3. ClassPathBeanDefinitionScanner를 사용한 빈 등록 과정: Scan 메서드 호출
3-1. scan 메서드 호출
- scan 메서드는 클래스 경로 스캔의 시작점이다. 이 메서드의 주요 작업은 다음과 같다.
1. 스캔 전 빈(Bean)의 개수 확인
- this.registry.getBeanDefinitionCount()를 호출하여 스캔 전 BeanDefinition의 총 개수를 확인한다.
2. 스캔 실행
- doScan(basePackages) 메서드를 호출하여 주어진 패키지(들) 내의 클래스들을 스캔한다. doScan 메서드는 실제로 클래스 경로를 스캔하고 적합한 컴포넌트를 식별하는 역할을 한다.
3. 어노테이션 구성 프로세서 등록
- AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)를 호출하여 필요한 경우 어노테이션 기반 구성 프로세서들을 BeanDefinition 레지스트리에 등록한다.
4. 스캔 후 새로 추가된 빈 개수 계산
- 스캔 후의 BeanDefinition의 총 개수에서 스캔 전 BeanDefinition의 총 개수를 뺌으로써, 새로 추가된 빈의 수를 반환한다.
이제 scan 메서드가 호출하는 doScan 메서드에 대해 상세히 알아보자
4. ClassPathBeanDefinitionScanner를 사용한 빈 등록 과정: doScan 메서드 호출
4-1. doScan 메서드의 호출
- ClassPathBeanDefinitionScanner클래스에서 BeanDefinition을 생성하기 위해 scan 메서드를 호출하면 이 메서드의 내부에서는 doScan 메서드를 호출한다.
4-2. doSacn 메서드 로직 알아보기
- doScan 메서드는 실제로 지정된 패키지에서 클래스를 스캔하고, 적합한 클래스에 대한 BeanDefinition을 생성 및 등록하는 주된 작업을 수행한다.
doScan 메서드 내부의 상세한 동작을 알아보자
5. doScan 메서드의 동작 상세히 알아보기
5-1. 패키지 검증하기 (doScan 메서드 동작 1번)
- 주어진 패키지가 비어 있지 않은지 확인한다(Assert.notEmpty).
5-2. 클래스 식별: findCandidateComponents 호출하기 (doScan 메서드 동작 2번)
- findCandidateComponents 메서드는 지정된 패키지 내에서 스프링의 어노테이션이 적용된 클래스들(예: @Component, @Service 등)을 찾는다. 이 메서드는 BeanDefinition 객체를 아직 생성하지 않고, 해당 클래스들을 식별하는 역할만 한다.
findCandidateComponents 메서드가 호출되어 스프링 어노테이션이 적용된 클래스를 식별하는 역할을 한다는건 알겠다. 그러나 중요한것은 내부의 상세한 동작 과정이라고 생각한다. 우리는 지금부터 그 내부의 동작에 대해서 자세히 알아볼 것이다.
5-3. findCandidateComponents 메서드 상세히 알아보기
ClassPathBeanDefinitionScanner 클래스의 findCandidateComponents 메서드는 지정된 패키지 내의 클래스들을 스캔하여 후보 컴포넌트를 찾는 역할을 한다. 이 메서드의 내부 로직은 대략적으로 다음과 같다.
- 만약 componentsIndex가 존재하고, 인덱스가 includeFilters를 지원하는 경우 addCandidateComponentsFromIndex 메서드를 호출하여 후보 컴포넌트를 찾는다. 그렇지 않은 경우, scanCandidateComponents 메서드를 호출하여 스캔을 수행한다.
1. 컴포넌트 인덱스 사용 여부 결정
- findCandidateComponents 메서드는 먼저 componentsIndex (컴포넌트 인덱스)가 사용 가능한지 확인한다. 이 인덱스는 스프링 어플리케이션 컨텍스트의 시작 시점에 어노테이션 처리를 통해 생성된 것일 수 있으며, 클래스 패스 스캔을 최적화하는 데 사용된다.
2. 인덱스를 사용하여 후보 컴포넌트 식별
- addCandidateComponentsFromIndex 메서드는 인덱스에서 해당 베이스 패키지와 관련된 후보 컴포넌트 타입들을 가져온다. 각각의 후보 타입에 대해, 메타데이터 리더를 사용하여 클래스의 메타데이터를 읽고, 이 클래스가 실제로 스프링 빈으로 등록될 수 있는 후보인지를 판단한다.
다음으로 findCandidateComponents 메서드 내부에서 호출되는 addCandidateComponentsFromIndex 메서드의 동작도 상세히 알아보자
5-4. addCandidateComponentsFromIndex 메서드의 동작
- 간단하게 설명하자면 findCandidateComponents 메서드에서 addCandidateComponentsFromIndex를 호출하는 경우, ScannedGenericBeanDefinition 객체를 생성하여 후보 컴포넌트로 추가한다. 이 단계에서 BeanDefinition 객체가 생성된다.
1. 타입 필터를 통한 컴포넌트 타입 식별 (addCandidateComponentsFromIndex 메서드 동작 1번)
- addCandidateComponentsFromIndex 메서드는 CandidateComponentsIndex를 사용하여 가장 먼저 인덱스에서 해당 베이스 패키지에 대한 후보 컴포넌트 타입들을 가져온다. 이는 includeFilters에 정의된 타입 필터를 통해 수행된다. 각 필터는 특정 스테레오타입(예: @Component, @Service 등)과 관련된 클래스를 식별하는 데 사용된다.
여기서 궁금한 점이 생겼다. 메서드 내부에서 this.includeFilters를 통해 접근을 하는데 이 값은 도대체 어떻게 세팅되는걸까? 이게 궁금해서 includeFilters는 어떻게 값을 가지는지에 대해 알아봤다.
2. includeFilters 알아보기
- includeFilters는 대체 어디에 선언되어있는지 찾아봤더니 addCandidateComponentsFromIndex 메서드가 선언되어 있는 ClassPathScanningCandidateComponentProvider 클래스 내부에 아래와 같이 멤버 변수로 includeFilters가 선언되어 있었다.
3. includeFilters는 어떻게 값이 세팅되는가?
- ClassPathBeanDefinitionScanner는 기본적으로 ClassPathScanningCandidateComponentProvider 클래스를 확장한다. 이 클래스는 includeFilters와 excludeFilters 리스트를 가지고 있다.
- ClassPathBeanDefinitionScanner의 생성자나 scan 메서드가 호출될 때, 스프링은 자동으로 기본 필터들을 includeFilters 리스트에 추가한다. 이 기본 필터들은 스프링의 핵심 어노테이션들(@Component, @Repository, @Service, @Controller)을 탐지하는데 사용된다.
includeFilters에 대한 궁금증은 해결했으니 addCandidateComponentsFromIndex의 다음 동작으로 돌아가자
4. 메타데이터 리더를 사용한 컴포넌트 확인 (addCandidateComponentsFromIndex 메서드 동작 2번)
- 동작 1번에서 "this.includeFilters"를 이용하여 필터링된 컴포넌트 타입들을 가져왔으니 이제 이 타입들을 바탕으로 실제 BeanDefinition 객체를 생성하고, 최종적으로 이 객체들을 반환한다.
- 후보 타입들에 대해서 메타데이터 리더(MetadataReader)를 사용하여 클래스의 메타데이터를 읽는다. isCandidateComponent() 메서드는 이 메타데이터를 기반으로 해당 클래스가 실제로 스프링 빈으로 등록될 수 있는 후보인지를 판단한다.
5. BeanDefinition 생성 (addCandidateComponentsFromIndex 메서드 동작 3번)
- 조건을 만족하는 클래스에 대해 ScannedGenericBeanDefinition 객체(sbd)가 생성되며, 이 객체는 해당 클래스의 메타데이터(예: 어노테이션 정보)와 리소스 정보를 포함한다. 이 객체들은 스프링 컨테이너에 의해 빈으로 등록될 수 있는 후보 컴포넌트의 정의를 나타낸다.
6. BeanDefinition 저장 (addCandidateComponentsFromIndex 메서드 동작 4번)
- 생성한 ScannedGenericBeanDefinition 객체(sbd)를 candidates.add(sbd) 메서드를 호출해서 Set<BeanDefinition> 타입의 candidates에 담아준다.
7. 예외 처리 (addCandidateComponentsFromIndex 메서드 동작 5번)
- I/O 관련 예외가 발생하면, BeanDefinitionStoreException을 발생시켜 이를 처리한다.
결과적으로, addCandidateComponentsFromIndex() 메서드는 classPath에서 효율적으로 컴포넌트(클래스들)를 찾고, 각 클래스에 대한 BeanDefinition을 생성하여 스프링 컨테이너에서 관리될 수 있도록 한다. 이 과정은 특히 큰 어플리케이션에서 성능 최적화를 위해 중요하다.
지금까지 doScan 내부에서 동작하는 findCandidateComponents 메서드의 동작을 알아봤다. 굉장히 복잡한 과정이라 쉽게 풀어서 설명하기가 쉽지 않았다. 지금 우리는 호출되는 순서를 따라가면서 확인하고 있으니 이해가 가지 않으면 한번 천천히 따라가보거나 디버깅을 해보는것을 추천한다. 지금부터는 다시 기존의 상위 호출인 doScan 메서드로 돌아가서 findCandidateComponents 메서드 호출 다음에 호출되는 동작들을 알아보자
6. findCandidateComponents 메서드로 받아온 BeanDefinition 객체 처리하기
6-1. doScan 메서드의 3~7번 동작 설명
6-2. 받아온 BeanDefinition을 for문으로 순회한다. (doScan 메서드 동작 3번)
- findCandidateComponents 호출을 마치고 BeanDefition을 반환한 후에 다시 상위 호출 메서드인 doScan 메서드로 돌아와서 다음 로직으로 for문을 실행한다. for문 내부에서는 findCandidateComponents 메서드에서 반환받은 BeanDefinition 객체들(candidates)을 순회하며 동작한다.
6-3. Scope 메타데이터 설정 진행 (doScan 메서드 동작 4번)
- doScan 메서드 내에서 ScopeMetadataResolver는 BeanDefinition에 설정된 클래스의 스코프 정보를 결정하는 데 사용된다. 여기서 scopeMetadata는 candidate로 식별된 클래스에 대한 스코프 정보를 해석한다.
- candidate.setScope(scopeMetadata.getScopeName());는 이 해석된 스코프 정보를 BeanDefinition 객체에 설정한다. 이 과정을 통해 각 빈의 스코프(예: 싱글톤, 프로토타입)가 결정되며, 이후 스프링 컨테이너에서 해당 스코프에 따라 빈의 생명주기가 관리된다.
6-4.빈(Bean) 이름 생성 (doScan 메서드 동작 5번)
- this.beanNameGenerator.generateBeanName() 메서드는 각 BeanDefinition에 대한 고유한 이름을 생성하는 데 사용된다.
6-5. BeanDefinition 처리 - AbstractBeanDefinition (doScan 메서드 동작 6-1번)
1. 후보 클래스가 AbstractBeanDefinition의 인스턴스인 경우
- AbstractBeanDefinition는 스프링 프레임워크에서 빈(Bean)의 정의를 나타내는 핵심 클래스 중 하나다. 이 클래스는 스프링 컨테이너가 빈을 생성하고 관리하는 데 필요한 정보를 담고 있다.
간단히 말해, AbstractBeanDefinition은 스프링 빈에 대한 설정과 메타데이터를 포함한다. 이 정보에는 빈의 클래스 타입, 생성 방식, 생명주기 콜백, 의존성 정보, 스코프(예: 싱글톤, 프로토타입) 등이 포함된다.
2. AbstractBeanDefinition 인스턴스에 대한 처리
- postProcessBeanDefinition 메서드는 해당 빈의 세부적인 구성을 조정한다. 이 메서드는 빈의 자동 와이어링 모드, 초기화 및 파괴 메서드, 그리고 빈의 스코프와 같은 기본 설정을 다룬다. 이러한 설정들은 빈의 생명주기와 스프링 컨테이너 내에서의 동작 방식에 직접적인 영향을 미친다. 결과적으로, 이 단계는 빈의 생성 및 관리 방법을 더욱 정밀하게 조정하는 기회를 제공하며, 스프링 어플리케이션의 성능과 안정성을 높이는 데 중요한 역할을 한다.
6-6. BeanDefinition 처리 - AnnotatedBeanDefinition (doScan 메서드 동작 6-2번)
1. 후보 클래스가 AnnotatedBeanDefinition의 인스턴스인 경우
- AnnotatedBeanDefinition은 스프링 프레임워크에서 어노테이션 기반의 빈 정의를 나타내는 인터페이스다. 이 인터페이스는 BeanDefinition을 확장하며, 어노테이션에 기반한 추가적인 메타데이터를 제공한다. 주로 @Component, @Service, @Repository, @Controller와 같은 스테레오타입 어노테이션이 적용된 클래스에 사용된다.
2. AnnotatedBeanDefinition 인스턴스에 대한 처리
- AnnotatedBeanDefinition 인스턴스의 처리 과정 AnnotationConfigUtils.processCommonDefinitionAnnotations 메서드 호출을 통해 이루어진다. 이 메서드는 BeanDefinition에 적용된 주요 어노테이션들을 분석하고 처리한다.
- 처리되는 어노테이션에는 @Lazy, @Primary, @DependsOn, @Role, @Description 등이 포함된다. 이러한 어노테이션들은 빈의 생성 시점, 우선 순위, 의존성 관계, 역할 및 설명과 같은 핵심적인 특성을 정의한다. 따라서 이 단계는 빈의 동작 방식에 대한 추가적인 구성을 제공하며, 어플리케이션 내에서 빈(Bean)이 보다 유연하고 효과적으로 작동할 수 있도록 돕는다.
6-7. BeanDefinition 검증 및 등록 (doScan 메서드 동작 7번)
- checkCandidate 메서드는 생성된 빈 이름에 대한 유효성을 검사하고, 이름 충돌이 없는지 확인한다.
- BeanDefinitionHolder 객체는 candidate로부터 생성된 BeanDefinition과 해당 빈의 이름을 포함하며, 이는 빈 정의의 일종의 "포장(wrapper)" 역할을 한다.
- registerBeanDefinition 메서드를 호출하여 BeanDefinitionHolder에 포함된 BeanDefinition을 BeanDefinitionRegistry에 등록한다.
6-8. doScan 메서드 종료 (Set<BeanDefinitionHolder> 반환)
- 마지막으로, 이 Set<BeanDefinitionHolder>를 반환한다. 이 Set에는 스캔된 패키지 내에서 찾아낸 모든 유효한 빈 후보들의 BeanDefinitionHolder 객체들이 포함되어 있으며, 이들은 스프링 컨테이너에 의해 관리되는 빈으로 등록될 준비가 되어 있다.
- 요약하면, doScan 메서드는 지정된 패키지들에서 빈 후보들을 찾고, 이를 적절히 처리하여 최종적으로 스프링 컨테이너에 등록될 준비가 완료된 BeanDefinitionHolder 객체들의 집합을 반환한다.
6-9. 요약하자면 이 내용은 다음과 같다.
요약하면, doScan 메서드는 지정된 패키지들에서 빈 후보들을 찾고, 이를 적절히 처리하여 최종적으로 스프링 컨테이너에 등록될 준비가 완료된 BeanDefinitionHolder 객체들의 집합을 반환한다.
스프링은 내부적으로 어떻게 빈을 등록하는지 상세히 알아봤다. 다음 포스트는 @ComponentScan 이후의 빈 생명주기, 즉 인스턴스화, 의존성 주입, 초기화 등의 과정을 다루게 될 예정이다.
스프링은 Java Reflection을 활용하여 생성자 주입을 진행한다. 이 내용도 궁금하다면 읽어보는것을 추천한다.
시간이 괜찮다면 팀원 '평양냉면 7'님의 블로그도 한번 봐주세요 :)
'Spring 기초 > Spring 기초 지식' 카테고리의 다른 글
가볍게 알아보는 디자인 패턴 - 팩토리 메서드 패턴(Factory Method Pattern) (1) | 2023.12.11 |
---|---|
가볍게 알아보는 디자인 패턴 - 싱글톤 패턴(Singleton Pattern) (1) | 2023.12.11 |
SpringBoot: 리소스 관리하기 (resource) (1) | 2023.11.25 |
[Spring] @Component와 @Bean의 차이점 (0) | 2023.11.13 |
[Spring] @Component로 스프링 빈 등록하기 (0) | 2023.11.12 |