[Spring] Spring IoC 컨테이너와 SpringApplication 실행 흐름 정리 (Spring Boot 3.3.3 기준)

이명규·2024년 9월 16일
post-thumbnail

Spring에 대한 정리글입니다. 주로 IoC 컨테이너, BeanFactory, 그리고 Spring Boot 애플리케이션의 시작 흐름에 대해 다룹니다.

참고: SpringApplication 클래스는 Spring Boot에서만 사용되며, 일반 Spring에서는 수동으로 초기화가 필요합니다.


스프링 IoC 컨테이너

BeanFactory

  • Spring IoC 컨테이너의 최상위 인터페이스
  • BeanFactory 공식 문서
  • 애플리케이션 구성 요소의 중앙 레지스트리 역할

ApplicationContext

  • 가장 많이 사용하는 IoC 컨테이너
  • BeanFactory를 확장하며 다양한 기능 포함: EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, ResourceLoader
  • ApplicationContext 공식 문서

ListableBeanFactory

  • 여러 빈을 나열하거나 조회 가능
  • getBeanNamesForType(), getBeansOfType() 등 제공
    • 특정 유형의 빈 목록 조회 가능
  • ListableBeanFactory 공식 문서

HierarchicalBeanFactory

  • 부모 빈 팩토리를 참조할 수 있는 기능
  • 계층적 구조로 빈을 탐색하며 존재하지 않을 경우 부모 팩토리에서 탐색
    • getParentBeanFactory() 메서드를 통해 현재 빈 팩토리의 부모 빈 팩토리를 알 수 있으며 만약, 없을 경우 null 을 반환
    • 계층적인 구조로 되어 있으므로 빈을 먼저 현재 빈 팩토리에서 찾고 없을 경우 부모 빈 팩토리에서 계속해서 탐색(setParentBeanFactory(parentFactory))
  • HierarchicalBeanFactory 공식 문서

ResourceLoader

  • 리소스 로딩 기능 (클래스패스, 파일 시스템 등)
    • 웹에서의 파일 등 리소스를 로드하게끔 하는 기능을 가진 인터페이스
  • ResourcePatternResolver를 통해 여러 리소스를 위치 패턴으로 검색 가능
  • ResourceLoader 공식 문서



Spring Boot 의 시작점

Spring Boot 애플리케이션은 main() 메서드 내에서 SpringApplication.run()을 호출하면서 시작됩니다.

public static void main(String[] args) {
    SpringApplication.run(MyApplication.class, args);
}
  • ApplicationContext 인스턴스를 생성하며 빈들을 스캔하고, 설정에 따라 각 빈을 생성하고 구성합니다.

  • 그 과정에서 다양한 이벤트가 발생하며, 미리 등록된 리스너들이 해당 이벤트를 수신하여 처리합니다.

  • application.yml 또는 application.properties 파일, 그리고 커맨드라인 인자에서 환경설정을 읽고 이를 환경 변수로 등록합니다.

이 전체 과정은 내부적으로 BeanFactory와 연결되며, IoC 컨테이너가 실제로 동작하는 핵심 흐름과도 밀접합니다


  • HierarchicalBeanFactory는 현재 빈 팩토리뿐 아니라 상위 빈 팩토리를 참조할 수 있는 구조를 가지고 있습니다.

  • 빈 조회는 getBean() 메서드를 통해 수행되며, 대부분의 실제 운영 환경에서는 이 메서드를 직접 호출하지 않고, 주입받는 방식(@Autowired 등)을 통해 사용됩니다.

  • 빈을 주입받는 과정에서, 컨테이너는 이름이나 타입에 기반하여 알맞은 빈을 검색합니다.

    • 빈 조회 방식 :
      주입할 빈을 찾을 때, 먼저 현재 컨텍스트에서 우선적으로 검색한 후 해당 빈이 존재하지 않을 경우 상위 컨텍스트 (부모 빈 팩토리) 로 검색을 확장
      이는 HierarchicalBeanFactory 가 제공하는 계층적 탐색 구조에 기반합니다
  • 이후, refreshContext() 메서드가 호출되어 IoC 컨테이너의 최종 초기화가 이루어지며, 모든 빈이 등록되고 초기화됩니다.


번외) ListableBeanFactory 는 언제 사용될까 ?

  • 여러 개의 빈을 동시에 조회하거나, 특정 조건에 맞는 빈을 나열할 때 유용합니다.
  • 대부분의 Spring 애플리케이션에서는 개발자가 직접 사용하는 경우는 드물고, Spring 내부 매커니즘 또는 자동 구성 기능에서 활용됩니다.

예시:

  • 자동 구성 (Auto-Configuration)
    • 자동 구성 과정에서 특정 타입의 빈을 일괄적으로 탐색해야 할 때 getBeansOfType() 메서드가 활용됩니다.
  • 조건부 구성 (Conditional Configuration)
    • @Conditional 어노테이션과 함께 사용되며, 특정 조건에 맞는 빈을 활성화하거나 비활성화할 때 내부적으로 빈 목록을 확인해야 할 필요가 있습니다.
  • 초기화 시 빈 확인
    • 애플리케이션 컨텍스트 초기화 시 모든 빈이 제대로 등록되었는지 확인하는 과정에서 getBeanDefinitionNames() 등의 메서드가 호출되며, 설정 누락이나 충돌 여부를 검증하는 데 사용됩니다.

참고 : 왜 객체를 빈으로 등록할까 ?

  • 의존성 관리 (IoC)
    • 외부 객체에 의존하는 로직을 테스트하기 쉬움 (예: Mock 주입)
    • 비즈니스 도메인이 특정 객체에 의존하고 있다면, 의존성 주입을 통해 가짜 객체(Mock)로 대체하여 독립적인 테스트가 가능함
  • 스코프 관리
    • singleton: 동일한 인스턴스를 재사용해야 하는 경우 유용
    • prototype: 요청마다 새로운 인스턴스를 생성해야 할 때 사용
  • 라이프사이클 관리
    • 객체 초기화 및 소멸 단계에서 필요한 작업을 자동으로 처리할 수 있음
    • @PostConstruct, @PreDestroy, 혹은 InitializingBean, DisposableBean 인터페이스를 통해 정의 가능

SpringApplication.run()

public ConfigurableApplicationContext run(String... args) {
    Startup startup = SpringApplication.Startup.create();
    
    if (this.registerShutdownHook) {
        shutdownHook.enableShutdownHookAddition();
    }

    DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
    ConfigurableApplicationContext context = null;
    this.configureHeadlessProperty();
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);

    Throwable ex;
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        Banner printedBanner = this.printBanner(environment);
        context = this.createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        this.refreshContext(context);
        this.afterRefresh(context, applicationArguments);
        startup.started();
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), startup);
        }

        listeners.started(context, startup.timeTakenToStarted());
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
        ex = var10;
        throw this.handleRunFailure(context, ex, listeners);
    }

    try {
        if (context.isRunning()) {
            listeners.ready(context, startup.ready());
        }

        return context;
    } catch (Throwable var9) {
        ex = var9;
        throw this.handleRunFailure(context, ex, (SpringApplicationRunListeners)null);
    }
}

run() 메서드는 Spring Boot 애플리케이션이 시작될 때 내부적으로 호출되는 핵심 메서드입니다.
이 메서드 내부에서는 애플리케이션의 컨텍스트를 구성하고, 필요한 설정을 초기화하며, 사용자 정의 로직을 실행하기 위한 준비 과정을 수행합니다.

이후의 설명에서는 각 단계별 역할과 작동 방식에 대해 소스 기반으로 자세히 설명합니다.


1. Startup 초기화

  • 애플리케이션의 실행 시간 측정을 위해 Startup 객체를 생성합니다.
  • 이 객체는 애플리케이션의 시작 시간, 프로세스 가동 시간 등의 정보를 기록하며, 나중에 로깅 등에 활용됩니다.
abstract static class Startup {
	private Duration timeTakenToStarted;

    Startup() {
    }

	protected abstract long startTime();
	protected abstract Long processUptime();
	protected abstract String action();
    ...
}     

사용 예시:

public ConfigurableApplicationContext run(String... args) {
    Startup startup = SpringApplication.Startup.create();
    ...
}
  • run() 메서드의 가장 첫 줄에서 Startup.create() 를 통해 해당 인스턴스를 생성합니다.

2. 종료 후크 설정(Shutdown Hook)

if (this.registerShutdownHook) {
    shutdownHook.enableShutdownHookAddition();
}
  • registerShutdownHook 플래그가 true일 경우, 애플리케이션 종료 시 실행될 후처리 작업을 등록합니다.

  • 이는 JVM이 종료될 때 실행되는 스레드이며, 스프링 컨텍스트가 종료되지 않은 경우 이를 안전하게 종료하고 관련 리소스를 정리합니다.

  • 내부적으로는 AbstractApplicationContext.registerShutdownHook() 메서드가 호출되어 doClose() 를 수행하게 되며, 이 과정에서 모든 싱글톤 빈의 @PreDestroy 메서드나 DisposableBean 구현체가 호출됩니다.


3. BootstrapContext 생성

  • createBootstrapContext()를 호출하여 초기 설정에 사용할 임시 컨텍스트를 생성합니다.

  • Spring Boot 애플리케이션의 초기화 과정에서 사용되는 일종의 임시 컨텍스트이며 ApplicationContext 가 완전히 준비되기 전에 초기화 작업에 필요한 리소스와 설정을 관리, 애플리케이션 시작 시 여러 설정 작업을 돕는 역할을 수행합니다.

  • 부트스트랩 컨텍스트는 전체 컨텍스트에 공유되어야 하는 초기 컴포넌트를 저장하고, 이후 실제 컨텍스트 초기화 시 함께 넘겨집니다.


4. 사전 준비 작업

  • context 변수 선언 (실제 ApplicationContext 가 나중에 할당될 자리)

  • configureHeadlessProperty() 호출: 서버 환경(GUI 없는 환경)에서 문제 없이 실행될 수 있도록 AWT 환경 설정을 비활성화합니다.

  • 실행 과정에서 발생하는 다양한 이벤트 처리를 위한 리스너 초기화

  • 초기화된 리스너들에게 애플리케이션이 시작되고 있음을 알리기 위해 starting 이벤트를 발생

  • 애플리케이션 실행 시 전달된 커맨드라인 인자를 처리하기 위해 ApplicationArguments 객체를 생성

    • 애플리케이션이 시작할 때 전달된 인자들을 파싱하고 관리하는 역할

5. 환경(Environment) 설정

ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
  • prepareEnvironment() 는 다음과 같은 여러 환경 소스를 통합하여 Environment 객체를 구성합니다 (런타임시에 이루어짐)
    • 시스템 환경 변수
    • JVM 시스템 프로퍼티
    • application.yml 혹은 application.properties
    • -key=value 형식의 커맨드라인 인자
  • 설정이 완료되면 ApplicationEnvironmentPreparedEvent 이벤트가 발행되며, 이를 통해 리스너들이 환경 관련 후처리를 진행할 수 있게 됩니다.

6. 배너 출력

Banner printedBanner = this.printBanner(environment);
  • 기본적으로 Spring Boot 로고가 ASCII 아트 형태로 출력되며, 환경 설정 또는 커스텀 배너 클래스를 통해 변경 가능함.

7. ApplicationContext 생성

context = this.createApplicationContext();
  • ApplicationContextFactory 인터페이스를 통해 생성되며 DEFAULT = new DefaultApplicationContextFactory(); 로 생성 (커스텀 팩토리 추가 가능)
    • webApplicationType 에 맞는 컨텍스트 생성
      • NONE: CLI 또는 백그라운드 작업 (서블릿 X)
      • SERVLET: 서블릿 기반 웹 애플리케이션 (Tomcat, Jetty 등)
      • REACTIVE: WebFlux 기반의 비동기/논블로킹 애플리케이션
  • webApplicationType 은 클래스패스에 있는 의존성을 통해 자동으로 감지되며, 우선순위는 REACTIVE > SERVLET > NONE 순서입니다.
    • Reactor Netty 와 같은 반응형 서버가 있다면 REACTIVE로 설정
    • spring-boot-starter-web 의존성이라면 서블릿 기반 애플리케이션을 만듦

8. context 초기화 및 실행 준비 완료

this.prepareContext(...);
  • 생성된 ApplicationContext 에 필요한 정보(환경, 리스너, 설정값 등)를 주입하는 단계
  • 아직 컨텍스트는 리프레시 (refresh) 되지 않은 상태이며, 다음과 같은 작업이 수행됨
    • Environment 주입
    • 리스너 등록
    • 초기화할 빈 설정
    • 부트스트랩 컨텍스트에서 필요한 Bean 전달

참고
환경 설정이 다시 필요한 이유 ?
부트스트랩 컨텍스트에서 이미 일부 환경 설정을 수행했지만 애플리케이션 컨텍스트의 환경을 설정하고 보강하는 작업이 추가로 필요하다.

  • 전체 애플리케이션의 컨텍스트의 환경(Environment) 을 설정하며 부트스트랩에서 설정되지 않은 추가 환경 변수, 프로파일, 설정 파일을 처리함
  • 리스너 설정은 왜 또 하는건가 ?
    • prepareContext 에서는 리스너가 애플리케이션 실행 과정에서 발생하는 이벤트들을 처리하도록 세팅하는 부분
    • listeners.starting() 은 애플리케이션 시작 시의 이벤트만을 처리하는 초기 단계이며 이후 단계에서 리스너들은 다른 종류의 이벤트 (ContextRefreshedEvent 등) 을 처리할 준비를 함
  • 리프레시 상태란 ?
    • 애플리케이션 컨텍스트가 완전히 초기화되고 모든 빈이 생성되고 설정이 끝난 상태를 말함
    • refresh() 가 완료되면 모든 빈이 준비되고 이벤트가 발행되며 애플리케이션이 실행될 준비가 완전히 완료된 상태가 됨
    • 이 과정이 끝나야 애플리케이션이 정상적으로 작동할 수 있음

9. Context 리프레시

this.refreshContext(context);
  • 내부적으로 AbstractApplicationContext.refresh()가 호출되며 본격적인 IoC 컨테이너 초기화가 진행됨

  • 주요 단계

    • 빈 팩토리 생성 및 설정 정보 로딩
    • @ComponentScan 및 수동 등록 빈 처리
    • BeanPostProcessor, BeanFactoryPostProcessor 실행
      • 이 단계에서 빈의 설정을 변경하거나 추가적인 처리를 할 수 있음
    • 싱글톤 빈 초기화
    • ContextRefreshedEvent 발행
      • 해당 이벤트 리스너들이 이벤트를 처리

10. 후처리 및 실행

  • afterRefresh() : 컨텍스트가 완전히 초기화된 후 추가 작업을 수행하기 위해 호출, 기본 구현은 비어 있음
  • Startup.started() : 실행 시간 측정을 종료
  • StartupInfoLogger 를 통해 애플리케이션 시작 로그 출력
  • listeners.started() : 시작 완료 이벤트 발행
  • callRunners() : ApplicationRunner, CommandLineRunner 를 구현한 Bean들을 호출

11. 애플리케이션 준비 완료

if (context.isRunning()) {
    listeners.ready(context, startup.ready());
}
  • 애플리케이션이 정상적으로 실행되고 있는 경우 ApplicationReadyEvent 를 발행하여 모든 설정 및 로딩이 완료되었음을 알립니다.
profile
개발자

0개의 댓글