IoC에서 어떻게 빈이 생성되는가

@MinSang · August 29, 2024 · 15 min read

이때 설명하기 쉽게 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);
        }
    }
}

ConfigurationClassPostProcessorprocessConfigBeanDefinitions 메소드에서 다음과 같은 과정이 진행됩니다

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이 붙은 클래스를 포함합니다. 그 후, 이 클래스들을 파싱하는 과정에서 컴포넌트 스캔이 시작됩니다.

컴포넌트 스캔

ConfigurationClassParserdoProcessConfigurationClass 메소드에서 @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 참조로 저장
                }
            }
    }
}
}

그림1
그림1

SpingBootApplication의 클래스가 속한 패키지 경로에 있는 모든 하위 클래스들을 컴포넌트 스캔하는 과정입니다.(물론 excludeFilters 지정하면 해당 경로는 스캔 안하게 할 수 있습니다.)

@Component가 있는 클래스들을 BeanDefinition 형태로 저장합니다.

BeanDefinition 정보

img 2

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 에 저장합니다.

그외에도 ProxyTransactionManagementBookChallengeApplication의 ConfigurationClass 도 configurationClasses에 저장됩니다.

ConfigurationClass에는 클래스 메타데이터, 빈이름, 클래스 위치, 빈 메소드들(리턴타입,메소드이름 등) 들이 각각 저장되어있습니다.

이렇게 계속 패키지 내 ConfigurationClass 뿐아니라 그외에도 @SpringBootApplication 내에는 Auto-configuration 어노테이션이 있는데 이 자동구성 때문에 META-INF/spring.factories 파일에 자동 구성 클래스들도 역시 읽어옵니다.

92개 정도의 Configuration이 등록됐습니다.
92개 정도의 Configuration이 등록됐습니다.

빈 정의 등록

public class ConfigurationClassPostProcessor {
    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        ...
        Set<ConfigurationClass> configClasses = new LinkedHashSet(parser.getConfigurationClasses()); // 92개의 ConfigurationClass의 keySet 반환
        
        ... 
        // BeanDefinitionRegistry BeanDefinition 등록 과정
        
    }
}

BeanDefinitionRegistry에 BeanDefinition들을 등록합니다. 이때 Configuration에 속한 빈들이 모두 등록되는 과정입니다.

img 4

img 6

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 프레임워크에서는 먼저, 설정 클래스들을 처리합니다.

  1. 클래스 스캐닝: Spring은 먼저 애플리케이션의 클래스패스를 스캔합니다. 이는 주로 @ComponentScan 어노테이션이나 XML 설정을 통해 지정된 패키지들을 대상으로 합니다.
  2. @Configuration 클래스 식별: 스캐닝 과정에서 @Configuration 어노테이션이 붙은 클래스들을 찾습니다. 이는 리플렉션(Reflection) API를 사용하여 수행됩니다.
  3. Bean 정의 생성: 찾아낸 @Configuration 클래스들에 대해, Spring은 해당 클래스 내의 @Bean 어노테이션이 붙은 메서드들을 분석합니다. 각 @Bean 메서드는 하나의 Bean 정의로 변환됩니다.
  4. Bean 정의 등록: 생성된 Bean 정의들은 Spring의 BeanDefinitionRegistry에 등록됩니다. 이 레지스트리는 나중에 실제 Bean 인스턴스를 생성할 때 사용됩니다.
  5. 의존성 분석: Spring은 등록된 Bean 정의들 사이의 의존 관계를 분석합니다. 이는 @Autowired, @Inject 등의 어노테이션이나 생성자 파라미터를 통해 이루어집니다.
  6. 추가 후처리: 필요에 따라 Bean 정의들에 대한 추가적인 처리가 이루어질 수 있습니다. 예를 들어, BeanFactoryPostProcessor를 통한 Bean 정의 수정 등이 여기에 포함됩니다.

이 과정은 주로 리플렉션과 Spring의 내부 API를 사용하여 수행됩니다. ClassPathBeanDefinitionScannerConfigurationClassPostProcessor 같은 Spring의 내부 클래스들이 이 작업을 담당합니다.

빈 정의(BeanDefinition)가 등록된 후, Spring은 실제 빈 인스턴스를 생성합니다.

이 과정은 다음과 같이 진행됩니다.

  1. 빈 생성 및 초기화
  2. 의존성 주입: 빈이 생성된 후, 설정된 의존성을 주입합니다. 이는 생성자 주입, 세터 주입, 또는 필드 주입을 통해 이루어집니다.
  3. 초기화 메서드 호출: 빈의 초기화 메서드(@PostConstruct 어노테이션이 붙은 메서드나 InitializingBean 인터페이스의 afterPropertiesSet() 메서드)가 호출됩니다.
  4. AOP 프록시 생성: 필요한 경우, AOP 프록시가 생성되어 원본 빈을 감싸게 됩니다.

이 과정을 거쳐 빈의 실제 인스턴스가 생성되고 Spring 컨테이너에 등록됩니다.

이후 애플리케이션에서 ApplicationContext를 통해 빈을 요청하면, 이미 생성된 인스턴스가 반환됩니다.

번외: 스프링의 IoC가 뭔가요?

IoC(제어의 역전)는 소프트웨어 엔지니어링에서 사용되는 프로그래밍 원칙으로, 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 말합니다.

Spring 프레임워크에서 IoC는 다음과 같이 구현됩니다.

  • 빈 생성과 생명주기 관리: 개발자가 직접 객체를 생성하고 관리하는 대신, Spring 컨테이너가 이를 대신 수행합니다.
  • 의존성 주입 (Dependency Injection): 객체가 필요로 하는 의존성을 외부(Spring 컨테이너)에서 주입합니다. 이를 통해 객체 간의 결합도를 낮추고 유연성을 증가시킵니다.
  • 설정의 외부화: 애플리케이션의 구성과 동작을 XML, Java Config, 또는 어노테이션을 통해 외부에서 정의할 수 있습니다.
  • AOP (Aspect-Oriented Programming) 지원: 핵심 비즈니스 로직과 부가적인 기능(로깅, 트랜잭션 등)을 분리하여 관리할 수 있습니다.
@MinSang
지식과 경험을 기록하는 TIL 저장소