스프링 컨테이너와 빈(Bean) - ApplicationContext 동작 과정

Computer Science/Spring Boot

스프링 컨테이너: 왜 컨테이너가 필요할까?

IoC / DI의 본질을 알아보자

먼저 IoC란 Inversion of Control의 약자로 그대로 풀이하면 "제어의 역전"이다. 객체 생성, 생명주기, 의존성 연결 등 제어 흐름을 애플리케이션 코드가 아닌 외부(프레임워크)가 주도하는 것이다.
단순하게 IoC에 대한 정의는 쉽게 찾아볼 수 있다. 근데 IoC는 어떤 문제를 해결하기 위해 나왔을까?

IoC는 왜 생겼고 이걸 쓰면 뭐가 좋을까?

역시 구글은 모르는게 없어

구글을 찾아보니 IoC는 객체 간의 결합도를 낮추고 유지보수성과 테스트 용이성을 높이기 위해 고안되었다고 한다. 그럼 IoC는 어떻게 해당 문제를 해결할 수 있었을까? 먼저 결론을 얘기하자면 DI로 해당 문제를 해결하였다.

 

원래 객체는 스스로 협력 대상을 new로 만들고, 생명주기를 직접 관리한다. IoC(제어의 역전)에선 이 권한이 컨테이너로 넘어간다.

간략하게 설명하자면 다음과 같다.

생성(creation), 조립(wiring), 수명관리(lifecycle) → 컨테이너 담당

객체는 “무엇이 필요하다”만 선언(=의존성) -> DI(의존성 주입)은 IoC의 대표 구현이다.

하지만 IoC는 DI만 구현하는 것이 아니다. 예를 들어 서블릿 컨테이너가 doGet()을 호출, JUnit이 테스트를 실행, 이벤트 루프가 콜백을 호출하는 등 코드의 전반적인 흐름을 외부에서 제어한다.

 

그럼 왜 객체 간의 결합을 낮춰야할까?

객체 간의 강한 결합은 코드의 유연성을 떨어트린다. 객체가 직접 의존성을 생성하고 관리하는 방식을 확인해보자.

예를 들면 A클래스가 B 클래스를 사용할 때 A가 직접 B의 인스턴스를 new로 생성하는 방식이다.

class OrderService {
    private EmailNotifier notifier;
    
    public OrderService() {
        this.notifier = new EmailNotifier(); // 직접 생성
    }
    
    public void processOrder(Order order) {
        // 주문 처리 로직
        notifier.send("주문이 완료되었습니다.");
    }
}

이러한 예제가 있다고 생각해보자. 정말 베이직한 코드지만 위처럼 코드를 작성한다면 아래와 같은 문제점이 발생한다.
 
1. 변경 요구사항에 대응하기 어려움
* 고객이 "이메일 대신 SMS로 알림을 보내고 싶다"고 하면 OrderService 코드를 직접 수정해야 한다.
-> new EmailNotifier()를 new SmsNotifier()로 바꿔야 한다.
2. 다양한 환경에서 사용하기 어려움
* 개발 환경에서는 콘솔 출력으로 테스트하고 싶다면?
* 운영 환경에서는 이메일, 테스트 환경에서는 로그만 남기고 싶다면?
-> 각각 다른 OrderService를 만들거나 코드를 계속 수정해야 한다.
3. 확장성 부족
"이메일과 SMS를 동시에 보내고 싶다"는 요구사항이 생기면?
-> OrderService 내부 로직을 완전히 다시 작성해야 한다
4. 테스트하기 어려움
OrderService를 테스트할 때 실제 이메일이 발송된다! 심지어 이메일 서버가 죽으면 OrderService 테스트도 실패하게 된다.
 
이처럼 객체간의 강한 결합은 코드를 재사용하기도 힘들고 유지보수하기 너무 복잡하다... 이런 문제를 해결하기 위해 IoC를 적용해본다면

class OrderService {
    private Notifier notifier; // 인터페이스에 의존
    
    public OrderService(Notifier notifier) { // 외부에서 결정
        this.notifier = notifier;
    }
    
    public void processOrder(Order order) {
        // 주문 처리 로직 (변경 없음)
        notifier.send("주문이 완료되었습니다.");
    }
}

어떤 "Notifier를 사용할지"의 제어권이 외부로 이동한다. OrderService는 더 이상 구체적인 구현체를 알 필요가 없다! 특히 SOILD 원칙의 OCP(Open-Closed principle - 확장에는 열려있되 변경에는 닫혀있어야 함)와 DIP(Dependency Inversion Principle)에 직접적으로 연관이 있다.
 
테스트 코드에서도 마찬가지로

@Test
void testUserService() {
    MockRepository mockRepo = new MockRepository();
    UserService service = new UserService(mockRepo);
    
    // 실제 DB 없이도 테스트 가능
    service.createUser("test");
    verify(mockRepo).save(any(User.class));
}

간편하게 가짜 의존성만 만들어 테스트할 수 있다.
 
이제 DI에 대해서 알아보자. DI란 Dependency Injection의 약자로 그대로 풀이하면 "의존성 주입"이다. 객체에 필요한 의존성을 외부에서 주입하는 것이다. 뭔가 IoC랑 비슷한거 같은데..? 라는 느낌이 든다! DI는 기본적으로 IoC 원칙이 적용된 구현 방법으로 IoC는 설계 원칙이라면 DI는 IoC를 구현하는 구체적인 패턴 중 하나라고 생각하면 될 것 같다.
 

스프링 컨테이너의 정체

위에서 설명한듯이 IoC 패턴은 확장성의 측면에서 매우 중요한데, Spring에서 IoC를 실현하는 런타임 인프라가 "스프링 컨테이너"이다.(DI 컨테이너, IoC 컨테이너 라고 부르기도 함!) DI 또한 스프링 컨테이너가 제공해주는 기능이다. "컨테이너"라는 단어에서 알 수 있듯이 객체(빈(bean)이라고 부름)를 보관하고 관리한다.
 

스프링 빈(Bean)

스프링은 IoC, 리소스/라이프사이클 누수, 트랜잭션/보안 등의 문제를 "컨테이너"를 제시하여 해결했다. 코드의 전체적인 제어 흐름이 컨테이너로 역전되며 컨테이너는 할 일이 많아졌다... 그 중 이번엔 스프링 빈(Bean)에 관련한 부분을 알아보자
 
스프링 빈(Bean)은 스프링 컨테이너가 관리하는 객체이다. 일반적인 Java 객체와는 달리 스프링 컨테이너에 의해 생성, 관리, 소멸되는 객체를 의미한다. DI에 사용되는 객체도 모두 스프링 빈(Bean)이다!
 
전체적인 흐름은 다음과 같다.

스프링 빈 등록과정

개발자는 빈으로 등록될 클래스를 Configuration 파일에 지정하고 컨테이너가 해당 파일을 스캔한다. 이후 등록된 빈으로 스프링 컨테이너가 직접 의존성을 주입해준다.
 

Spring Boot 소스코드 Deep Dive

빈이 컨테이너에 생성되는 과정을 직접 Spring Boot의 소스코드를 확인해보며 알아보자.

애플리케이션 시작: SpringApplication.run()

public ConfigurableApplicationContext run(String... args) {
    Startup startup = Startup.create();
    if (this.properties.isRegisterShutdownHook()) {
        SpringApplication.shutdownHook.enableShutdownHookAddition();
    }
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();

    // 리스너 준비 및 시작 이벤트
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        Banner printedBanner = printBanner(environment);

        // ApplicationContext (스프링 컨테이너) 생성
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);

        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

        // ApplicationContext 새로고침 (빈 등록의 핵심)
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        Duration timeTakenToStarted = startup.started();
        if (this.properties.isLogStartupInfo()) {
            new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
        }
        listeners.started(context, timeTakenToStarted);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        throw handleRunFailure(context, ex, listeners);
    }
    try {
        if (context.isRunning()) {
            listeners.ready(context, startup.ready());
        }
    }
    catch (Throwable ex) {
        throw handleRunFailure(context, ex, null);
    }
    return context;
}

 
실제 Spring Boot 4.0.0-SNAPSHOT 버전의 소스코드이다. SNAPSHOT 버전이라 이후 변경사항이 있을 수 있다. 이 하나의 메서드에서 Hello World부터 복잡한 애플리케이션까지 모든 스프링 부트 애플리케이션의 시작을 담당하는 것이 정말 흥미롭다. 아래 코드처럼 스프링 프로젝트에 기본적으로 생성되는 코드에서도 ctl + 클릭으로 확인할 수 있지만 최신 Spring Boot의 흐름이 어떻게 되는지 궁금해서 아래 깃허브에서 확인해보았다 (org.springframework.boot 패키지의 SpringApplication 클래스)

public static void main(String[] args) {
    SpringApplication.run(SpringApplication.class, args);
}

https://github.com/spring-projects/spring-boot

 

GitHub - spring-projects/spring-boot: Spring Boot helps you to create Spring-powered, production-grade applications and services

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss. - spring-projects/spring-boot

github.com

 

컨테이너 생성: ApplicationContext 결정 과정

스프링 컨테이너 생성 및 특별한 빈 등록
SpringApplication.run()이 호출되고 애플리케이션이 실행된 이후 환경 준비, 배너출력 이후 스프링 컨테이너가 생성된다.

// ApplicationContext (스프링 컨테이너) 생성
context = createApplicationContext();

// createApplicationContext() 메서드
protected ConfigurableApplicationContext createApplicationContext() {
    ConfigurableApplicationContext context = this.applicationContextFactory
        .create(this.properties.getWebApplicationType());
    Assert.state(context != null, "ApplicationContextFactory created null context");
    return context;
}

// SpringApplication.class의 applicationContextFactory 값
private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;

// ApplicationContextFactory 인터페이스의 DEFAULT
public interface ApplicationContextFactory {

	/**
	 * A default {@link ApplicationContextFactory} implementation that will create an
	 * appropriate context for the {@link WebApplicationType}.
	 */
	ApplicationContextFactory DEFAULT = new DefaultApplicationContextFactory();
    
    ...
}

// 위 ApplicationContextFactory 기본 구현의 내부 메서드
public class DefaultApplicationContextFactory implements ApplicationContextFactory {
    
    @Override
    public ConfigurableApplicationContext create(@Nullable WebApplicationType webApplicationType) {
        try {
            return getFromSpringFactories(webApplicationType, ApplicationContextFactory::create,
                    this::createDefaultApplicationContext);
        }
        catch (Exception ex) {
            throw new IllegalStateException("Unable create a default ApplicationContext instance, "
                    + "you may need a custom ApplicationContextFactory", ex);
        }
    }
    
    // 기본 구현체
    private ConfigurableApplicationContext createDefaultApplicationContext() {
        if (!AotDetector.useGeneratedArtifacts()) {
            return new AnnotationConfigApplicationContext();
        }
        return new GenericApplicationContext();
    }
    
    @Contract("_, _, !null -> !null")
    private <T> @Nullable T getFromSpringFactories(@Nullable WebApplicationType webApplicationType,
            BiFunction<ApplicationContextFactory, @Nullable WebApplicationType, @Nullable T> action,
            @Nullable Supplier<T> defaultResult) {
            
        // spring.factories에서 ApplicationContextFactory 구현체들 찾기
        for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,
                getClass().getClassLoader())) {
                
            // 각 후보에 대해 ApplicationContext 생성 시도
            T result = action.apply(candidate, webApplicationType);
            
            if (result != null) {
                return result;		// 성공하면 반환
            }
        }
        
	    // 3. 모든 후보가 실패하면 기본 구현 사용
        return (defaultResult != null) ? defaultResult.get() : null;
    }
}

// getFromSpringFactories 메서드에서 구현체들을 찾는 방식
// 대규모 오픈소스답게 주석이 매우 친절하다 (길어서 삭제, 대충 Spring.factories 파일에 정의되어있는 구현체를 찾는다는 설명)
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
    return forDefaultResourceLocation(classLoader).load(factoryType);
}

getFromSpringFactories에서 후보를 찾을 때 사용하는 loadFactories에 주석으로 명시되어 있는 방법을 보고 Spring.factories 파일에 어떤 후보가 있는지 찾아봤지만 놀랍게도 ApplicationContextFactory의 구현체가 정의되어있지 않다!! 그 말은 기본적으론 AnnotationConfigApplicationContext() 혹은 GenericApplicationContext() 만 사용하는 것 같다. 아마 커스텀 ApplicationContextFactory를 구현해서 사용할 사람을 위해 남겨놓은 듯 하다. 기회가 된다면 직접 구현해보는 것도 재미있을 것 같다!
 
결국 AnnotationConfigApplicationContext() 혹은 GenericApplicationContext() 인데...

private ConfigurableApplicationContext createDefaultApplicationContext() {
    if (!AotDetector.useGeneratedArtifacts()) {
        return new AnnotationConfigApplicationContext();
    }
    return new GenericApplicationContext();
}

이 조건문을 확인해보면 AotDetector의 userGeneratedArtifacts() 메서드의 여부로 스프링 컨테이너가 결정된다.

/**
 * Utility for determining if AOT-processed optimizations must be used rather
 * than the regular runtime. Strictly for internal use within the framework.
 */
public abstract class AotDetector {

	public static final String AOT_ENABLED = "spring.aot.enabled";

	private static final boolean inNativeImage = NativeDetector.inNativeImage(Context.RUN, Context.BUILD);


	public static boolean useGeneratedArtifacts() {
		return (inNativeImage || SpringProperties.getFlag(AOT_ENABLED));
	}

}

이번에도 주석이 아주 친절하다. 런타임 대신 AOT 처리 최적화를 사용해야 하는지 결정하는 도구라는데 잘 감이 오진 않는다. 시스템 프로퍼티에서 직접 플래그를 True로 설정하거나 네이티브 이미지(?)라면 True를 반환하는 것 같다. 아무튼 일반적인 상황에서는 사용되지 않는 것 같으니 GenericApplicationContext()도 패스!
 
네이티브 이미지가 무엇인가 찾아보니 GraalVM Native Image 환경을 말하는 것 같다.
* AOT 관련 참고 블로그 및 공식문서
https://docs.spring.io/spring-boot/reference/packaging/native-image/introducing-graalvm-native-images.html
https://luvstudy.tistory.com/267
 
긴 여정을 거쳐 기본 스프링 컨테이너가 AnnotationConfigApplicationContext()로 확정되고 context 변수로 할당된다.
이후 컨테이너 기본 설정인 prepareContext()를 거쳐 빈이 등록된다.(Environment 설정, postProcess, 전용 싱글톤 빈 등록, BeanFactory 기본 설정 등등)

// AnnotationConfigApplicationContext의 기본 생성자(BeanDefinition Reader와 Scanner를 할당한다)
public AnnotationConfigApplicationContext() {
    StartupStep createAnnotatedBeanDefReader = getApplicationStartup().start("spring.context.annotated-bean-reader.create");
    this.reader = new AnnotatedBeanDefinitionReader(this);
    createAnnotatedBeanDefReader.end();
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}

// AnnotatedBeanDefinitionReader의 생성자들
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) {
    this(registry, getOrCreateEnvironment(registry));
}

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
    Assert.notNull(environment, "Environment must not be null");
    this.registry = registry;
    this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
    
    // 어노테이션 처리기(infrastructure bean)들 등록
    AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

// AnnotationConfigApplicationContext의 부모 클래스
public GenericApplicationContext() {
    this.beanFactory = new DefaultListableBeanFactory();
}

이후 컨테이너가 할당되면서 다음과 같은 핵심 BeanPostProcessor(ROLE_INFRASTRUCTURE의 빈)들이 자동으로 등록된다. 해당 빈은 빈을 처리하는 처리기 역할도 함.(먼저 등록되는 이유)

우리가 만든 config를 읽어 BeanDefinition을 만든다. XML등 다른 형식도 지원!

// registerAnnotationConfigProcessors에서 호출되는 빈 등록 메서드
private static BeanDefinitionHolder registerPostProcessor(
        BeanDefinitionRegistry registry, RootBeanDefinition definition, String beanName) {

    definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    registry.registerBeanDefinition(beanName, definition);
    return new BeanDefinitionHolder(definition, beanName);
}

// 여러 어노테이션 처리기들 (기본 registry만 있는 생성자로 호출하면 아래 생성자를 다시 호출함)
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
        BeanDefinitionRegistry registry, @Nullable Object source) {

    DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
    if (beanFactory != null) {
        if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
            beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
        }
        if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
            beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
        }
    }

    Set<BeanDefinitionHolder> beanDefs = CollectionUtils.newLinkedHashSet(6);
	
    if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
        RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
        def.setSource(source);
        beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
    }

    if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
        RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
        def.setSource(source);
        beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
    }
	
    // 이외 여러가지 BeanDefinition들
    ...

    return beanDefs;
}

이후 컴포넌트 스캔을 위한 ClassPathBeanDefinitionScanner가 생성된다.

this.scanner = new ClassPathBeanDefinitionScanner(this);

 

Bean 등록 과정: BeanDefinition 생성

스프링 빈 등록 및 생성

// run 메서드에서 호출된 컨테이너 새로고침 메서드
private void refreshContext(ConfigurableApplicationContext context) {
    if (this.properties.isRegisterShutdownHook()) {
        shutdownHook.registerApplicationContext(context);
    }
    refresh(context);
}

이후 프로퍼티에 따라 shutdownHook의 여부가 결정되고 실제 Context Refresh가 일어난다. shutdownHook은 JVM의 리소스 누수를 막아 우아한(?) 종료를 보장해주는 안전장치이다. 지금은 Bean에 대해서 공부하고 있으니 나중에 알아보도록 하자!

// Spring Application 클래스에서 호출하는 refresh 메서드
protected void refresh(ConfigurableApplicationContext applicationContext) {
    applicationContext.refresh();
}

// ConfigurableApplicationContext 인터페이스의 구현체인 AbstractApplicationContext의 refresh 메서드
// AnnotationConfigApplicationContext에는 refresh가 따로 구현되어있지 않아 해당 메서드를 그대로 사용한다
@Override
public void refresh() throws BeansException, IllegalStateException {
    this.startupShutdownLock.lock();
    try {
        this.startupShutdownThread = Thread.currentThread();
		
        ...
        
        // Tell the subclass to refresh the internal bean factory.
        // 1번 후보
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

            StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
            // Invoke factory processors registered as beans in the context.
            // 2번 후보
            invokeBeanFactoryPostProcessors(beanFactory);
            // Register bean processors that intercept bean creation.
            // 3번 후보
            registerBeanPostProcessors(beanFactory);
            beanPostProcess.end();

            ...

            // Instantiate all remaining (non-lazy-init) singletons.
            // 4번 후보
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        }

        ...
    }
    finally {
        this.startupShutdownThread = null;
        this.startupShutdownLock.unlock();
    }
}

코드가 너무 길어져 스킵했다..! 하지만 이번에도 주석이 아주 친절하게 달려있다. Bean 생성에 핵심적인 부분만 확인해보자

// 1번 후보: BeanFactory 생성 및 준비
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
    refreshBeanFactory();
    return getBeanFactory();
}

// GenericApplicationContext에 선언된 refreshed (초기값 = false)
private final AtomicBoolean refreshed = new AtomicBoolean();

// GenericApplicationContext에 구현됨
@Override
protected final void refreshBeanFactory() throws IllegalStateException {
    if (!this.refreshed.compareAndSet(false, true)) {
        throw new IllegalStateException(
                "GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
    }
    this.beanFactory.setSerializationId(getId());
}

// GenericApplicationContext에 선언된 beanFactory
private final DefaultListableBeanFactory beanFactory;

// GenericApplicationContext에 구현됨
@Override
public final ConfigurableListableBeanFactory getBeanFactory() {
    return this.beanFactory;
}

먼저 빈 팩토리를 생성 및 준비하는 단계이다. obtainFreshBeanFactory 메서드를 호출하여 beanFactory를 생성 및 준비한다.
obtainFreshBeanFactory가 호출하는 refreshBeanFactory 메서드는 protected final로 선언되어 하위 클래스가 변경하지 못하도록 보호한다. 따라서 일관된 동작을 보장할 수 있다.
또한 refresh가 여러 번 일어나지 않도록 초기화된 상태를 AtomicBoolean의 compareAndSet을 활용해 Thread-Safe하게 상태를 변경한다. 이후 setSerializationId로 빈 컨테이너의 식별과 관리를 위해 식별자를 달아주고 빈 팩토리를 가져온다.
 
* 왜 refresh가 여러 번 일어나면 안 될까?
-> GenericApplicationContext에 정의된 refresh 메서드는 기존 빈들을 정리하는 로직이 없다. 따라서 여러 스레드에서 refresh 된다면 기존 빈들에서 여러 메모리, 연결 누수가 발생할 수 있다.

// 2번 후보 BeanFactoryPostProcessor 실행
invokeBeanFactoryPostProcessors(beanFactory);

다음으로 가장 중요한 단계인 invokeBeanFactoryPostProcessors 단계이다. 이 단계에서 대부분의 빈들이 등록된다.

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    // 위에서 등록한 BeanFactoryPostProcessor들 실행
    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
	
    // LoadTimeWeaver 설정 (AOP 관련)
    // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
    // (for example, through an @Bean method registered by ConfigurationClassPostProcessor)
    if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null &&
            beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
        beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
        beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
    }
}

 

여기서 빈 등록의 핵심인 PostProcessorRegistrationDelegate의 invokeBeanFactoryPostProcessors가 실행된다. 코드 주석에 해당 메서드는 쉽게 리팩토링할 수 있는 것처럼 보일 수 있지만 PR을 남길 경우 모든 거부된 PR 목록을 검토하여 중대한 변경점이 없도록 해달라고 쓰여져있다..! ㄷㄷ
 
크게 흐름을 보자면 2단계로 나뉘어져 있다. 
1. BeanDefinitionRegistryPostProcessor들 처리

// PriorityOrdered 구현체들을 우선적으로 실행
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();

// PriorityOrdered 구현체들 찾기
String[] postProcessorNames =
        beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
    if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
        currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
        processedBeans.add(ppName);
    }
}

// 정렬 후 실행
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();

이 과정에서 ConfigurationClassPostProcess가 실행된다. 해당 postProcessor는 @Configuration, @ComponentScan, @Bean, @Import 등을 처리하여 대부분의 빈들이 이 단계에서 등록된다.
이후 우선순위에 따라 Ordered 구현체와 BeanDifinitionRegistryPostProcessor들이 실행된다.
 
2. 일반 BeanFactoryPostProcessor들 처리
여기서도 priorityOrdered 구현체들이 먼저 실행되고 Ordered 구현체와 나머지가 순서대로 실행된다.
 
그리고 코드를 확인해보면 새로운 PostProcessor가 등록될 경우를 대비하여 while 루프로 체크한다. (@Import로 추가 Congifuration 클래스가 등록되는 경우 등)

boolean reiterate = true;
while (reiterate) {
    reiterate = false;
    postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
    for (String ppName : postProcessorNames) {
        if (!processedBeans.contains(ppName)) {
            currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
            processedBeans.add(ppName);
            reiterate = true; // 새로운 PostProcessor가 등록되었으므로 다시 체크
        }
    }
    sortPostProcessors(currentRegistryProcessors, beanFactory);
    registryProcessors.addAll(currentRegistryProcessors);
    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
    currentRegistryProcessors.clear();
}

여기까지하면 대부분의 빈들은 BeanDefinition이 거의 등록된다. 빈을 사용하기 위한 "설계도"가 스프링 컨테이너에 모두 등록된 것이다.

BeanDefinition 생성

 

// 3번 후보 BeanPostProcessor 등록
registerBeanPostProcessors(beanFactory);

 
이 다음으론 registerBeanPostProcessors이다. 공부하면서 사실 이 부분이 조금 헷갈렸다. 위에서 postProcessor들은 다 등록한거 아닌가? 라는 생각이 들었는데 위의 invokeBeanFactoryPostProcessors는 BeanFactoryPostProcessor를 찾아서 실행시켜, 해당 PostProcessor들이 빈들을 찾아 등록한다. 하지만 registerBeanPostProcessors는 BeanDefinition에 등록된 BeanPostProcessor들을 찾아서 인스턴스화 하여 BeanFactory의 실행 리스트에 추가한다.
 

Bean 인스턴스화: 실제 객체 생성

// 4번 후보 남은 싱글톤 빈들 인스턴스화
finishBeanFactoryInitialization(beanFactory);

드디어 마지막으로 알아볼 메서드이다. 이 메서드는 Spring 컨테이너 초기화의 마지막 단계로 실제 Bean 인스턴스들을 생성한다.

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    // Initialize bootstrap executor for this context.
    if (beanFactory.containsBean(BOOTSTRAP_EXECUTOR_BEAN_NAME) &&
            beanFactory.isTypeMatch(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)) {
        beanFactory.setBootstrapExecutor(
                beanFactory.getBean(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class));
    }

    // Initialize conversion service for this context.
    if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
            beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
        beanFactory.setConversionService(
                beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
    }

    // Register a default embedded value resolver if no BeanFactoryPostProcessor
    // (such as a PropertySourcesPlaceholderConfigurer bean) registered any before:
    // at this point, primarily for resolution in annotation attribute values.
    if (!beanFactory.hasEmbeddedValueResolver()) {
        beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
    }

    // Call BeanFactoryInitializer beans early to allow for initializing specific other beans early.
    String[] initializerNames = beanFactory.getBeanNamesForType(BeanFactoryInitializer.class, false, false);
    for (String initializerName : initializerNames) {
        beanFactory.getBean(initializerName, BeanFactoryInitializer.class).initialize(beanFactory);
    }

    // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
    String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
    for (String weaverAwareName : weaverAwareNames) {
        try {
            beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class);
        }
        catch (BeanNotOfRequiredTypeException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Failed to initialize LoadTimeWeaverAware bean '" + weaverAwareName +
                        "' due to unexpected type mismatch: " + ex.getMessage());
            }
        }
    }

    // Stop using the temporary ClassLoader for type matching.
    beanFactory.setTempClassLoader(null);

    // Allow for caching all bean definition metadata, not expecting further changes.
    beanFactory.freezeConfiguration();

    // Instantiate all remaining (non-lazy-init) singletons.
    beanFactory.preInstantiateSingletons();
}

 
Bootstrap Executor, Conversion Service, Embedded Value Resolver, AOP Load-Time Weaving 등의 빈들을 인스턴스화 시켜 실제 사용 준비를 완료하고, freezeConfiguration으로 더 이상 BeanDefinition이 수정 불가능 하도록 설정한다. 마지막으로 beanFactory.preInstantiateSingletons()를 호출하여 실제 Bean 인스턴스를 생성한다.
* Lazy-Init이 아닌 싱글톤 Bean만 인스턴스 생성!
* 빈 등록(BeanDefinition)과 빈 인스턴스 생성은 다름! 헷갈리지 말기

생성자 주입을 사용해야하는 이유

스프링 의존성 주입을 공부하다보면 생성자 주입을 사용하라는 말을 들어본 적 있을 것이다. 심지어 스프링 공식에서도 생성자 주입 방식을 권장하고 있다! 왜 그럴까?

생성자 주입은 “빈 인스턴스화(생성자 호출)” 순간에 의존성이 함께 주입되기 때문에 순환 참조를 사전에 방지할 수 있기 때문이다.

컨테이너가 생성자를 고르고(단일 생성자/@Autowired가 붙은 생성자 우선), 필요한 의존 빈을 먼저 getBean 해서 생성자 인자로 넘긴 뒤 바로 호출한다.

여기서 생성자가 끝나는 시점에 해당 의존성들은 이미 완전히 주입 완료된 상태 (단, 의존 대상에 @Lazy가 붙었으면 프록시(지연 프록시)가 인자로 들어가고 실제 대상은 처음 사용할 때 만들어짐)

 

그럼 여기서 다른 빈이 의존성으로 필요한 경우엔 어떻게 될까? 라는 의문이 들 수 있다. 생성 시점에 모든 의존성이 주입된다면 B라는 빈에서 A라는 빈이 의존성으로 필요한데 B 생성 시점에 A가 생성되지 않은 경우는 어떻게 해결할까?

 

스프링은 “생성 순서”를 미리 고정해 두지 않고, 주입을 해보는 순간에 필요한 빈을 만들어 해결한다.

B가 A를 생성자 인자로 요구하면 컨테이너가 B를 만들기 전에 먼저 getBean(A)를 호출해 A를 생성합니다. A가 아직 없다면 그 자리에서 만들고(싱글톤이면 1회 생성·캐시에 보관), 그 인스턴스를 B의 생성자에 넣어줍니다.

B가 만들어질 때 A가 아직 없어도, 컨테이너가 그 자리에서 A를 먼저 만들어 주입한다. 여기서 예외는 순환 의존 뿐이기 때문에 순환 의존을 컴파일 시점에 확인할 수 있는 것이다.


이렇게 스프링 컨테이너와 빈 등록 및 생성에 대해서 알아보았다. 직접 코드를 따라가다보니 Thread-safe, 확장성 등 대규모 오픈소스에 대한 철학과 maintainer들의 섬세함을 엿볼 수 있었던 시간이 되었다.
 
* 해당 내용은 직접 공부하며 작성하여 잘못된 내용이 있을 수 있습니다. 수정, 추가할 내용이 있다면 댓글로 피드백해주세요! 감사합니다!

'Computer Science > Spring Boot' 카테고리의 다른 글

JPA N+1 문제  (0) 2025.09.02
Spring MVC와 Servlet  (0) 2025.08.29
스프링 컨테이너와 빈(Bean) - Lifecycle Callback과 Scope  (0) 2025.08.24
스프링 컨테이너와 빈(Bean) - 싱글톤과 의존관계 주입 방법  (0) 2025.08.21
대댓글 페이지네이션  (0) 2025.08.14
'Computer Science/Spring Boot' 카테고리의 다른 글
  • Spring MVC와 Servlet
  • 스프링 컨테이너와 빈(Bean) - Lifecycle Callback과 Scope
  • 스프링 컨테이너와 빈(Bean) - 싱글톤과 의존관계 주입 방법
  • 대댓글 페이지네이션
hojoo
hojoo
그냥 개발이 즐거운 사람
  • hojoo
    dev_record
    hojoo
  • 전체
    오늘
    어제
    • 분류 전체보기 (84)
      • Study (0)
        • 모든 개발자를 위한 HTTP 웹 기본 지식 (0)
        • Real MySQL 8.0 (0)
        • 친절한 SQL 튜닝 (0)
        • 도메인 주도 개발 시작하기 (0)
        • 대규모 시스템 설계 기초 (0)
      • Computer Science (68)
        • Problem Solving (30)
        • Data Structure (4)
        • Spring Boot (14)
        • DB (1)
        • Java (4)
        • OS (3)
        • Server (3)
        • Tech (0)
      • Security (16)
        • Reversing (15)
        • Assembly (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    2539
    Lena tutorial
    리버싱
    프로그래머스
    9421
    Header
    소수상근수
    13265
    PE header
    dreamhack.io
    Spring boot
    리버싱 핵심원리
    DB
    12033
    Reversing
    DP
    HTTP
    15973
    백준
    19622
    16946
    n+1
    x64dbg
    서버 증설 횟수
    자료구조
    bean
    servlet
    n^2 배열 자르기
    21278
    레나 튜토리얼
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
hojoo
스프링 컨테이너와 빈(Bean) - ApplicationContext 동작 과정
상단으로

티스토리툴바