이때 설명하기 쉽게 BookChallengeApplication을 시작점으로 하겠습니다.
@SpringBootApplication
public class BookChallengeApplication {
public static void main(String[] args) {
SpringApplication.run(BookChallengeApplication.class, args);
}
}
애플리케이션 컨택스트 생성
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
...
context = this.createApplicationContext();
}
protected ConfigurableApplicationContext createApplicationContext() {
// 여기에 중단점 설정
}
}
서블릿 기반인 경우, context의 구현체는 AnnotationConfigServletWebServerApplicationContext
빈 등록 과정
@Configuration 클래스 처리
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
...
context = this.createApplicationContext(); // AnnotationConfigServletWebServerApplicationContext
...
this.refreshContext(context);
}
private void refreshContext(ConfigurableApplicationContext context) {
this.refresh(context);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}
}
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
public void refresh() throws BeansException, IllegalStateException {
...
this.invokeBeanFactoryPostProcessors(beanFactory);
...
}
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, this.getBeanFactoryPostProcessors());
...
}
}
final class PostProcessorRegistrationDelegate{
public static void invokeBeanFactoryPostProcessors(...){
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
}
private static void invokeBeanDefinitionRegistryPostProcessors(Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {
Iterator var3 = postProcessors.iterator();
while(var3.hasNext()) {
...
postProcessor.postProcessBeanDefinitionRegistry(registry);
postProcessBeanDefRegistry.end();
}
}
}
public class ConfigurationClassPostProcessor{
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
...
} else if (this.factoriesPostProcessed.contains(registryId)) {
...
} else {
this.registriesPostProcessed.add(registryId);
this.processConfigBeanDefinitions(registry);
}
}
}
ConfigurationClassPostProcessor
의 processConfigBeanDefinitions
메소드에서 다음과 같은 과정이 진행됩니다
public class ConfigurationClassPostProcessor {
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// 초기 @Configuration 클래스들을 찾아 후보로 등록
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// 찾은 @Configuration 클래스들을 Set으로 변환
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
parser.parse(candidates); // 파싱 시작, 컴포넌트 스캔
parser.validate();
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
// 새로운 @Configuration 클래스들을 처리
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
candidates.clear();
// 새로 등록된 빈 정의들 중에서 @Configuration 클래스를 다시 찾아 candidates에 추가
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.isFullConfigurationClass(bd) ||
ConfigurationClassUtils.isLiteConfigurationClass(bd)) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
}
}
이 과정에서 @Configuration
클래스들을 찾고 처리합니다. 이는 @SpringBootApplication
이 붙은 클래스를 포함합니다.
그 후, 이 클래스들을 파싱하는 과정에서 컴포넌트 스캔이 시작됩니다.
컴포넌트 스캔
ConfigurationClassParser
의 doProcessConfigurationClass
메소드에서 @ComponentScan 어노테이션을 처리합니다
public class ConfigurationClassParser{
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter){
...
Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
...
}
}
class ComponentScanAnnotationParser {
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
...
Class[] var20 = componentScan.getClassArray("basePackageClasses");
var21 = var20.length;
for(var22 = 0; var22 < var21; ++var22) {
Class<?> clazz = var20[var22];
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
...
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
}
Class[] var20 = componentScan.getClassArray("basePackageClasses");
은
BookChallengeApplication클래스의 basePackageClasses의 속성값을 확인합니다.
하지만 비어있기 때문에 "com.flab.bookchallenge.BookChallengeApplication"의 패키지이름만 추출한 "com.flab.bookchallenge" 를 basePackages에 담습니다.
return scanner.doScan(StringUtils.toStringArray(basePackages));
에서 com.flab.book_challenge
을 스캔하기 시작합니다.
public class ClassPathBeanDefinitionScanner{
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet();
...
Set<BeanDefinition> candidates = this.findCandidateComponents(basePackage); // basePackage를 넘겨 Componenent 스캔 시작
}
}
public class ClassPathScanningCandidateComponentProvider{
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
return this.componentsIndex != null && this.indexSupportsIncludeFilters() ? this.addCandidateComponentsFromIndex(this.componentsIndex, basePackage) : this.scanCandidateComponents(basePackage);
}
}
public class ClassPathScanningCandidateComponentProvider{
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet();
try {
String var10000 = this.resolveBasePackage(basePackage);
String packageSearchPath = "classpath*:" + var10000 + "/" + this.resourcePattern; // packageSearchPath = "classpath*:com/flab/book_challenge/**/*.class"
Resource[] resources = this.getResourcePatternResolver().getResources(packageSearchPath); // 아래 그림 1과 같이 해당 경로에 있는 모든 경로를 가져옵니다. 여기에는 BookChallengeApplication 클래스 경로도 포함됩니다.
...
try{
if (this.isCandidateComponent(metadataReader)) { // 해당 클래스가 Component 어노테이션이 붙여져있는지 확인합니다.
...
candidates.add(sbd); // BeanDefinition 참조로 저장
}
}
}
}
}
SpingBootApplication의 클래스가 속한 패키지 경로에 있는 모든 하위 클래스들을 컴포넌트 스캔하는 과정입니다.(물론 excludeFilters 지정하면 해당 경로는 스캔 안하게 할 수 있습니다.)
@Component
가 있는 클래스들을 BeanDefinition
형태로 저장합니다.
BeanDefinition 정보
BeanDefinition에는 다음과 같은 정보들을 저장합니다.
- 빈의 클래스 이름 (실제 구현 클래스의 전체 경로)
- 빈의 스코프 (싱글톤, 프로토타입 등)
- 빈의 생성자 인자
- 빈의 프로퍼티 값
- 빈의 초기화 메소드, 소멸 메소드
- 빈의 지연 초기화 여부 (lazy-init)
- 기타 빈과 관련된 메타데이터
public class ClassPathBeanDefinitionScanner{
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet();
...
Set<BeanDefinition> candidates = this.findCandidateComponents(basePackage); // basePackage를 넘겨 Componenent 스캔 시작
...
return beanDefinitions;
}
}
beanDefinitions
집합은 스캔된 각 빈에 대해 다음을 포함합니다:
- 빈의 이름 (Spring 컨테이너에서 사용될 식별자)
- 빈의 정의 (클래스 위치, 스코프, 의존성 등의 메타데이터)
- 빈의 별칭 (있는 경우)
다음은 configuration 클래스 내에 있는 @Bean 메소드들을 BeanDefinition으로 정의합니다.
public class ConfigurationClassParser {
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter){
Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
...
this.parse(bdCand.getBeanClassName(), holder.getBeanName()); // 같은 클래스 parse로 이동
...
Set<MethodMetadata> beanMethods = this.retrieveBeanMethodMetadata(sourceClass); // dataSourceConfig 에 있던 빈 4개를 configClass의 beanMethods에 저장
Iterator var18 = beanMethods.iterator();
}
protected final void parse(@Nullable String className, String beanName) throws IOException {
...
this.processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
}
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter){
...
sourceClass = this.asSourceClass(configClass, filter);
do{
sourceClass = this.doProcessConfigurationClass(configClass, sourceClass, filter); // dataSourceConfig 넘김
}
}
}
beanMethod 까지 작업이 끝난 DataSourceConfig의 ConfigurationClass 는 configurationClasses라는 Map 에 저장합니다.
그외에도 ProxyTransactionManagement
와 BookChallengeApplication
의 ConfigurationClass 도 configurationClasses에 저장됩니다.
ConfigurationClass에는 클래스 메타데이터, 빈이름, 클래스 위치, 빈 메소드들(리턴타입,메소드이름 등) 들이 각각 저장되어있습니다.
이렇게 계속 패키지 내 ConfigurationClass 뿐아니라 그외에도 @SpringBootApplication 내에는 Auto-configuration 어노테이션이 있는데 이 자동구성 때문에 META-INF/spring.factories 파일에 자동 구성 클래스들도 역시 읽어옵니다.
빈 정의 등록
public class ConfigurationClassPostProcessor {
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
...
Set<ConfigurationClass> configClasses = new LinkedHashSet(parser.getConfigurationClasses()); // 92개의 ConfigurationClass의 keySet 반환
...
// BeanDefinitionRegistry BeanDefinition 등록 과정
}
}
BeanDefinitionRegistry에 BeanDefinition들을 등록합니다. 이때 Configuration에 속한 빈들이 모두 등록되는 과정입니다.
BeanDefinitionRegistry는
빈 팩토리(DefaultListableBeanFactory)에 구현돼있습니다. 따라서 beanDefinitionMap에 위의 사진처럼 볼 수가 있습니다.
빈 실제 인스턴스 생성 과정
public abstract class AbstractApplicationContext{
public void refresh() throws BeansException, IllegalStateException {
...
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
...
this.finishBeanFactoryInitialization(beanFactory); // 빈 실제 인스턴스 생성 과정
...
}
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory){
...
beanFactory.preInstantiateSingletons();
}
}
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
public void preInstantiateSingletons() throws BeansException {
List<String> beanNames = new ArrayList(this.beanDefinitionNames); // 254 개의 빈 이름 추출
//순회 시작
String beanName;
...
this.getBean(beanName);
}
}
이제 빈 이름을 넘긴 getBean 메서드가 처음 호출됐을 때 빈 인스턴스를 생성합니다.
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) {
...
// 이미 생성된 빈이 있는지 확인
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null) {
// 있다면 반환
return (T) getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
...
if (mbd.isSingleton()) {
sharedInstance = this.getSingleton(beanName, () -> {
try {
return this.createBean(beanName, mbd, args);
} catch (BeansException var5) {
this.destroySingleton(beanName);
throw var5;
}
});
beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
getSingleton 메소드 과정에는 생성한 인스턴스를 싱글톤 맵에 등록하는 과정이 있습니다.
그리고 이 빈 생성을 조금만 더 살펴보면 인스턴스 생성 뿐만 아니라 의존성 주입과정(DI)도 있습니다.
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// ...
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
// ...
return beanInstance;
}
protected Object doCreateBean(...) {
// 1. 인스턴스 생성
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
// 2. 프로퍼티 설정 및 의존성 주입
populateBean(beanName, mbd, instanceWrapper);
// 3. 초기화
exposedObject = initializeBean(beanName, exposedObject, mbd);
// ...
return exposedObject;
}
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
// ...
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
return;
}
}
}
}
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// 이름으로 자동 와이어링
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// 타입으로 자동 와이어링
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}
// ...
// 실제 속성 설정
applyPropertyValues(beanName, mbd, bw, pvs);
}
class DefaultListableBeanFactory{
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
}
상속 구조를 보면 다음과 같기 때문에 빈팩토리에서 등록된 싱글톤 빈들을 추출할 수 있습니다.
DefaultSingletonBeanRegistry
^
|
AbstractBeanFactory
^
|
AbstractAutowireCapableBeanFactory
^
|
DefaultListableBeanFactory
결론적으로, 빈을 꺼내는 과정은 다음과 같습니다:
- 사용자가 BeanFactory.getBean()을 호출합니다.
- 이 호출은 AbstractBeanFactory.doGetBean()으로 전달됩니다.
- doGetBean()은 DefaultSingletonBeanRegistry의 getSingleton() 메서드를 사용하여 빈을 조회합니다.
- 빈이 없으면 생성하고 DefaultSingletonBeanRegistry에 저장한 후 반환합니다.
정리
Spring 프레임워크에서는 먼저, 설정 클래스들을 처리합니다.
- 클래스 스캐닝:
Spring은 먼저 애플리케이션의 클래스패스를 스캔합니다. 이는 주로
@ComponentScan
어노테이션이나 XML 설정을 통해 지정된 패키지들을 대상으로 합니다. @Configuration
클래스 식별: 스캐닝 과정에서@Configuration
어노테이션이 붙은 클래스들을 찾습니다. 이는 리플렉션(Reflection) API를 사용하여 수행됩니다.- Bean 정의 생성:
찾아낸
@Configuration
클래스들에 대해, Spring은 해당 클래스 내의@Bean
어노테이션이 붙은 메서드들을 분석합니다. 각@Bean
메서드는 하나의 Bean 정의로 변환됩니다. - Bean 정의 등록:
생성된 Bean 정의들은 Spring의
BeanDefinitionRegistry
에 등록됩니다. 이 레지스트리는 나중에 실제 Bean 인스턴스를 생성할 때 사용됩니다. - 의존성 분석:
Spring은 등록된 Bean 정의들 사이의 의존 관계를 분석합니다. 이는
@Autowired
,@Inject
등의 어노테이션이나 생성자 파라미터를 통해 이루어집니다. - 추가 후처리:
필요에 따라 Bean 정의들에 대한 추가적인 처리가 이루어질 수 있습니다. 예를 들어,
BeanFactoryPostProcessor
를 통한 Bean 정의 수정 등이 여기에 포함됩니다.
이 과정은 주로 리플렉션과 Spring의 내부 API를 사용하여 수행됩니다. ClassPathBeanDefinitionScanner
나 ConfigurationClassPostProcessor
같은 Spring의 내부 클래스들이 이 작업을 담당합니다.
빈 정의(BeanDefinition)가 등록된 후, Spring은 실제 빈 인스턴스를 생성합니다.
이 과정은 다음과 같이 진행됩니다.
- 빈 생성 및 초기화
- 의존성 주입: 빈이 생성된 후, 설정된 의존성을 주입합니다. 이는 생성자 주입, 세터 주입, 또는 필드 주입을 통해 이루어집니다.
- 초기화 메서드 호출: 빈의 초기화 메서드(@PostConstruct 어노테이션이 붙은 메서드나 InitializingBean 인터페이스의 afterPropertiesSet() 메서드)가 호출됩니다.
- AOP 프록시 생성: 필요한 경우, AOP 프록시가 생성되어 원본 빈을 감싸게 됩니다.
이 과정을 거쳐 빈의 실제 인스턴스가 생성되고 Spring 컨테이너에 등록됩니다.
이후 애플리케이션에서 ApplicationContext를 통해 빈을 요청하면, 이미 생성된 인스턴스가 반환됩니다.
번외: 스프링의 IoC가 뭔가요?
IoC(제어의 역전)는 소프트웨어 엔지니어링에서 사용되는 프로그래밍 원칙으로, 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 말합니다.
Spring 프레임워크에서 IoC는 다음과 같이 구현됩니다.
- 빈 생성과 생명주기 관리: 개발자가 직접 객체를 생성하고 관리하는 대신, Spring 컨테이너가 이를 대신 수행합니다.
- 의존성 주입 (Dependency Injection): 객체가 필요로 하는 의존성을 외부(Spring 컨테이너)에서 주입합니다. 이를 통해 객체 간의 결합도를 낮추고 유연성을 증가시킵니다.
- 설정의 외부화: 애플리케이션의 구성과 동작을 XML, Java Config, 또는 어노테이션을 통해 외부에서 정의할 수 있습니다.
- AOP (Aspect-Oriented Programming) 지원: 핵심 비즈니스 로직과 부가적인 기능(로깅, 트랜잭션 등)을 분리하여 관리할 수 있습니다.