main 메서드에서 SpringApplication.run()을 호출하면 로그가 쏟아지다가 "Started in X seconds"가 나옵니다. 그 사이에 정확히 무슨 일이 벌어지고 있는 걸까요?

전체 시작 흐름 요약

PLAINTEXT
main()

SpringApplication 인스턴스 생성
 ↓ ① 웹 타입 판별 (SERVLET / REACTIVE / NONE)
 ↓ ② 초기화 구성요소 로드 (spring.factories / AutoConfiguration.imports)

run() 메서드 실행
 ├─ ③ Environment 준비 (프로퍼티, 프로필)
 ├─ ④ ApplicationContext 생성
 ├─ ⑤ 빈 정의 등록 (@ComponentScan + AutoConfiguration)
 ├─ ⑥ refresh() — 빈 생성, 의존성 주입, 초기화
 │   └─ 내장 웹 서버 시작 (Tomcat/Netty)
 ├─ ⑦ Runner 실행 (CommandLineRunner / ApplicationRunner)
 └─ ⑧ ApplicationReadyEvent 발행

① 웹 애플리케이션 타입 판별

SpringApplication은 클래스패스를 분석하여 애플리케이션 유형을 결정합니다.

JAVA
// 내부 로직 (간략화)
if (isPresent("org.springframework.web.reactive.DispatcherHandler")
    && !isPresent("org.springframework.web.servlet.DispatcherServlet")) {
    return WebApplicationType.REACTIVE;
} else if (isPresent("javax.servlet.Servlet")) {
    return WebApplicationType.SERVLET;
} else {
    return WebApplicationType.NONE;
}
  • SERVLET: Spring MVC → AnnotationConfigServletWebServerApplicationContext
  • REACTIVE: Spring WebFlux → AnnotationConfigReactiveWebServerApplicationContext
  • NONE: 웹 서버 없음 → AnnotationConfigApplicationContext

② 초기화 구성요소 로드

META-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에서 다양한 구성요소를 로드합니다.

  • ApplicationContextInitializer — 컨텍스트 초기화 커스텀
  • ApplicationListener — 이벤트 리스너
  • AutoConfiguration 클래스 목록 — 자동 설정 후보

③ Environment 준비

PLAINTEXT
우선순위 (높은 순서):
1. 커맨드라인 인자 (--server.port=9090)
2. 시스템 프로퍼티 (-Dserver.port=9090)
3. OS 환경 변수
4. application-{profile}.yml
5. application.yml
6. @ConfigurationProperties 기본값
JAVA
// 이 시점에서 발생하는 이벤트
ApplicationEnvironmentPreparedEvent
// → 프로퍼티 소스 추가, 프로필 활성화 등을 이 이벤트에서 처리 가능

④~⑤ ApplicationContext 생성과 빈 등록

JAVA
// 1단계: 사용자 정의 빈 스캔
@SpringBootApplication  // = @ComponentScan + @EnableAutoConfiguration + @Configuration
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }
}

@ComponentScan이 먼저 실행되어 사용자가 정의한 @Component, @Service, @Repository, @Controller 빈을 등록합니다.

JAVA
// 2단계: 자동 설정 적용
// @EnableAutoConfiguration이 AutoConfiguration.imports에 등록된
// 자동 설정 클래스들을 로드하고 @Conditional 조건을 평가

@AutoConfiguration
@ConditionalOnClass(DataSource.class)        // DataSource가 클래스패스에 있을 때만
@ConditionalOnMissingBean(DataSource.class)  // 사용자가 직접 정의하지 않았을 때만
public class DataSourceAutoConfiguration {
    // HikariCP DataSource 빈 자동 등록
}

사용자가 정의한 빈이 우선이고, 자동 설정은 빈이 없을 때 보완하는 역할입니다.

⑥ refresh() — 핵심 단계

AbstractApplicationContext.refresh()는 Spring Framework의 핵심이며 12개 이상의 단계로 구성됩니다.

JAVA
public void refresh() {
    // 1. BeanFactory 준비
    prepareBeanFactory(beanFactory);

    // 2. BeanFactoryPostProcessor 실행
    //    → @Configuration 클래스 파싱, @Bean 메서드 등록
    invokeBeanFactoryPostProcessors(beanFactory);

    // 3. BeanPostProcessor 등록
    //    → AOP 프록시, @Autowired 처리기 등
    registerBeanPostProcessors(beanFactory);

    // 4. 메시지 소스, 이벤트 멀티캐스터 초기화
    initMessageSource();
    initApplicationEventMulticaster();

이어서 이벤트를 구독하는 리스너를 정의합니다.

JAVA
    // 5. onRefresh() — 내장 웹 서버 생성 및 시작
    onRefresh();  // ← Tomcat/Netty가 여기서 시작

    // 6. 리스너 등록
    registerListeners();

    // 7. 싱글톤 빈 인스턴스화
    //    → 의존성 주입, @PostConstruct 실행
    finishBeanFactoryInitialization(beanFactory);

    // 8. refresh 완료
    //    → ContextRefreshedEvent 발행
    finishRefresh();
}

빈 생성 순서

PLAINTEXT
빈 정의 등록 → 의존성 해결 → 인스턴스 생성 → @Autowired 주입
→ @PostConstruct 실행 → InitializingBean.afterPropertiesSet()
→ @Bean(initMethod) 실행

⑦ Runner 실행

모든 빈 초기화와 웹 서버 시작이 완료된 후 실행됩니다.

JAVA
@Component
@Order(1)  // 실행 순서 지정
public class DataInitRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // 초기 데이터 로드, 캐시 워밍업 등
        log.info("초기 데이터 로드 완료");
    }
}

이어서 @Component을 적용한 나머지 구현부입니다.

JAVA
@Component
@Order(2)
public class HealthCheckRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // ApplicationArguments로 --name=value 형태의 인자 파싱 가능
        if (args.containsOption("init-mode")) {
            String mode = args.getOptionValues("init-mode").get(0);
            log.info("초기화 모드: {}", mode);
        }
    }
}

⑧ 시작 이벤트 순서

PLAINTEXT
ApplicationStartingEvent         ← 가장 먼저 (로깅 초기화 전)

ApplicationEnvironmentPreparedEvent  ← Environment 준비 완료

ApplicationContextInitializedEvent   ← Context 생성, 빈 미등록

ApplicationPreparedEvent            ← 빈 정의 등록 완료

ContextRefreshedEvent              ← refresh() 완료

WebServerInitializedEvent          ← 웹 서버 시작

ApplicationStartedEvent            ← Runner 실행 전

ApplicationReadyEvent              ← 모든 준비 완료 ✅
JAVA
@Component
public class StartupListener {

    @EventListener(ApplicationReadyEvent.class)
    public void onReady() {
        log.info("애플리케이션 준비 완료 — 트래픽 수신 가능");
        // 캐시 워밍업, 외부 서비스 연결 확인 등
    }

    @EventListener(ApplicationStartedEvent.class)
    public void onStarted() {
        log.info("애플리케이션 시작됨 — Runner 실행 전");
    }
}

시작 과정 디버깅

--debug 플래그

BASH
java -jar myapp.jar --debug

이렇게 실행하면 자동 설정 보고서가 출력됩니다.

PLAINTEXT
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:  (적용된 자동 설정)
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource'

Negative matches:  (적용되지 않은 자동 설정)
-----------------
   MongoAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient'

시작 시간 분석

YAML
# application.yml
spring:
  main:
    lazy-initialization: true  # 지연 초기화로 시작 시간 단축 (주의 필요)

management:
  endpoint:
    startup:
      enabled: true  # 시작 단계별 소요 시간 기록
JAVA
// Spring Boot 3.2+
// 시작 과정을 단계별로 기록
SpringApplication app = new SpringApplication(MyApp.class);
app.setApplicationStartup(new BufferingApplicationStartup(2048));
app.run(args);

Actuator로 시작 정보 확인

BASH
curl http://localhost:8080/actuator/startup

시작 시간 최적화

전략효과주의
지연 초기화 (lazy-initialization: true)시작 시간 단축첫 요청 시 지연 발생
불필요한 AutoConfiguration 제외빈 생성 감소필요한 설정을 잘못 제외할 수 있음
JVM 클래스 데이터 공유 (CDS)클래스 로딩 시간 단축JDK 설정 필요
GraalVM Native Image극적인 시작 시간 단축빌드 시간 증가, 제약 있음
JAVA
// 특정 AutoConfiguration 제외
@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    SecurityAutoConfiguration.class
})
public class MyApp { }

Graceful Shutdown

애플리케이션이 종료될 때 진행 중인 요청을 완료한 후 종료합니다.

YAML
server:
  shutdown: graceful  # 기본: immediate

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 최대 대기 시간

종료 순서:

  1. 새 요청 수신 중단
  2. 진행 중인 요청 완료 대기
  3. @PreDestroy 실행
  4. 빈 소멸
  5. ApplicationContext 종료

주의할 점

1. lazy-initialization을 켜면 첫 요청에서 타임아웃이 발생할 수 있다

spring.main.lazy-initialization=true로 설정하면 시작 시간은 줄어들지만, 빈 생성이 첫 요청 시점으로 지연됩니다. DB 커넥션 풀 초기화, 캐시 워밍업 등이 모두 첫 요청에서 발생하여 응답 시간이 수 초 이상 걸릴 수 있습니다. 특히 헬스체크가 성공한 직후 트래픽을 받으면 첫 사용자들이 타임아웃을 경험합니다.

2. CommandLineRunner에서 예외가 발생하면 애플리케이션 전체가 종료된다

CommandLineRunnerApplicationRunnerrun() 메서드에서 예외가 발생하면, 스프링부트가 이를 시작 실패로 간주하고 전체 애플리케이션을 종료합니다. 초기 데이터 로드나 외부 서비스 연결 확인에서 일시적 장애가 나면 서비스가 아예 뜨지 않습니다. Runner에서 필수가 아닌 작업은 try-catch로 감싸거나 ApplicationReadyEvent 리스너로 분리하세요.

3. @SpringBootApplication의 exclude를 잘못 설정하면 필요한 자동 설정이 빠진다

시작 시간 최적화를 위해 @SpringBootApplication(exclude = {...})로 자동 설정을 제외할 때, 의존성 체인을 파악하지 못하고 필요한 설정까지 제외하면 런타임에 NoSuchBeanDefinitionException이 발생합니다. 예를 들어 DataSourceAutoConfiguration을 제외하면 JpaRepositoriesAutoConfiguration도 동작하지 않습니다.

정리

  • SpringApplication.run()은 ** 웹 타입 판별 → Environment 구성 → 빈 등록 → refresh → Runner 실행** 순서로 진행됩니다.
  • @ComponentScan(사용자 빈)이 먼저, AutoConfiguration(자동 설정)이 나중에 적용됩니다.
  • 내장 웹 서버는 refresh()onRefresh() 단계에서 시작됩니다.
  • ApplicationReadyEvent 는 모든 준비가 완료된 시점입니다. 초기화 작업은 이 이벤트나 Runner에서 수행하세요.
  • --debug로 자동 설정 보고서를, Actuator /startup으로 시작 단계별 소요 시간을 확인할 수 있습니다.
댓글 로딩 중...