스프링 부트 시작 과정 — main 호출부터 요청 처리까지 벌어지는 일
main메서드에서SpringApplication.run()을 호출하면 로그가 쏟아지다가 "Started in X seconds"가 나옵니다. 그 사이에 정확히 무슨 일이 벌어지고 있는 걸까요?
전체 시작 흐름 요약
main()
↓
SpringApplication 인스턴스 생성
↓ ① 웹 타입 판별 (SERVLET / REACTIVE / NONE)
↓ ② 초기화 구성요소 로드 (spring.factories / AutoConfiguration.imports)
↓
run() 메서드 실행
├─ ③ Environment 준비 (프로퍼티, 프로필)
├─ ④ ApplicationContext 생성
├─ ⑤ 빈 정의 등록 (@ComponentScan + AutoConfiguration)
├─ ⑥ refresh() — 빈 생성, 의존성 주입, 초기화
│ └─ 내장 웹 서버 시작 (Tomcat/Netty)
├─ ⑦ Runner 실행 (CommandLineRunner / ApplicationRunner)
└─ ⑧ ApplicationReadyEvent 발행
① 웹 애플리케이션 타입 판별
SpringApplication은 클래스패스를 분석하여 애플리케이션 유형을 결정합니다.
// 내부 로직 (간략화)
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.factories와 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에서 다양한 구성요소를 로드합니다.
ApplicationContextInitializer— 컨텍스트 초기화 커스텀ApplicationListener— 이벤트 리스너AutoConfiguration클래스 목록 — 자동 설정 후보
③ Environment 준비
우선순위 (높은 순서):
1. 커맨드라인 인자 (--server.port=9090)
2. 시스템 프로퍼티 (-Dserver.port=9090)
3. OS 환경 변수
4. application-{profile}.yml
5. application.yml
6. @ConfigurationProperties 기본값
// 이 시점에서 발생하는 이벤트
ApplicationEnvironmentPreparedEvent
// → 프로퍼티 소스 추가, 프로필 활성화 등을 이 이벤트에서 처리 가능
④~⑤ ApplicationContext 생성과 빈 등록
// 1단계: 사용자 정의 빈 스캔
@SpringBootApplication // = @ComponentScan + @EnableAutoConfiguration + @Configuration
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
@ComponentScan이 먼저 실행되어 사용자가 정의한 @Component, @Service, @Repository, @Controller 빈을 등록합니다.
// 2단계: 자동 설정 적용
// @EnableAutoConfiguration이 AutoConfiguration.imports에 등록된
// 자동 설정 클래스들을 로드하고 @Conditional 조건을 평가
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // DataSource가 클래스패스에 있을 때만
@ConditionalOnMissingBean(DataSource.class) // 사용자가 직접 정의하지 않았을 때만
public class DataSourceAutoConfiguration {
// HikariCP DataSource 빈 자동 등록
}
사용자가 정의한 빈이 우선이고, 자동 설정은 빈이 없을 때 보완하는 역할입니다.
⑥ refresh() — 핵심 단계
AbstractApplicationContext.refresh()는 Spring Framework의 핵심이며 12개 이상의 단계로 구성됩니다.
public void refresh() {
// 1. BeanFactory 준비
prepareBeanFactory(beanFactory);
// 2. BeanFactoryPostProcessor 실행
// → @Configuration 클래스 파싱, @Bean 메서드 등록
invokeBeanFactoryPostProcessors(beanFactory);
// 3. BeanPostProcessor 등록
// → AOP 프록시, @Autowired 처리기 등
registerBeanPostProcessors(beanFactory);
// 4. 메시지 소스, 이벤트 멀티캐스터 초기화
initMessageSource();
initApplicationEventMulticaster();
이어서 이벤트를 구독하는 리스너를 정의합니다.
// 5. onRefresh() — 내장 웹 서버 생성 및 시작
onRefresh(); // ← Tomcat/Netty가 여기서 시작
// 6. 리스너 등록
registerListeners();
// 7. 싱글톤 빈 인스턴스화
// → 의존성 주입, @PostConstruct 실행
finishBeanFactoryInitialization(beanFactory);
// 8. refresh 완료
// → ContextRefreshedEvent 발행
finishRefresh();
}
빈 생성 순서
빈 정의 등록 → 의존성 해결 → 인스턴스 생성 → @Autowired 주입
→ @PostConstruct 실행 → InitializingBean.afterPropertiesSet()
→ @Bean(initMethod) 실행
⑦ Runner 실행
모든 빈 초기화와 웹 서버 시작이 완료된 후 실행됩니다.
@Component
@Order(1) // 실행 순서 지정
public class DataInitRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// 초기 데이터 로드, 캐시 워밍업 등
log.info("초기 데이터 로드 완료");
}
}
이어서 @Component을 적용한 나머지 구현부입니다.
@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);
}
}
}
⑧ 시작 이벤트 순서
ApplicationStartingEvent ← 가장 먼저 (로깅 초기화 전)
↓
ApplicationEnvironmentPreparedEvent ← Environment 준비 완료
↓
ApplicationContextInitializedEvent ← Context 생성, 빈 미등록
↓
ApplicationPreparedEvent ← 빈 정의 등록 완료
↓
ContextRefreshedEvent ← refresh() 완료
↓
WebServerInitializedEvent ← 웹 서버 시작
↓
ApplicationStartedEvent ← Runner 실행 전
↓
ApplicationReadyEvent ← 모든 준비 완료 ✅
@Component
public class StartupListener {
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
log.info("애플리케이션 준비 완료 — 트래픽 수신 가능");
// 캐시 워밍업, 외부 서비스 연결 확인 등
}
@EventListener(ApplicationStartedEvent.class)
public void onStarted() {
log.info("애플리케이션 시작됨 — Runner 실행 전");
}
}
시작 과정 디버깅
--debug 플래그
java -jar myapp.jar --debug
이렇게 실행하면 자동 설정 보고서가 출력됩니다.
============================
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'
시작 시간 분석
# application.yml
spring:
main:
lazy-initialization: true # 지연 초기화로 시작 시간 단축 (주의 필요)
management:
endpoint:
startup:
enabled: true # 시작 단계별 소요 시간 기록
// Spring Boot 3.2+
// 시작 과정을 단계별로 기록
SpringApplication app = new SpringApplication(MyApp.class);
app.setApplicationStartup(new BufferingApplicationStartup(2048));
app.run(args);
Actuator로 시작 정보 확인
curl http://localhost:8080/actuator/startup
시작 시간 최적화
| 전략 | 효과 | 주의 |
|---|---|---|
지연 초기화 (lazy-initialization: true) | 시작 시간 단축 | 첫 요청 시 지연 발생 |
| 불필요한 AutoConfiguration 제외 | 빈 생성 감소 | 필요한 설정을 잘못 제외할 수 있음 |
| JVM 클래스 데이터 공유 (CDS) | 클래스 로딩 시간 단축 | JDK 설정 필요 |
| GraalVM Native Image | 극적인 시작 시간 단축 | 빌드 시간 증가, 제약 있음 |
// 특정 AutoConfiguration 제외
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class
})
public class MyApp { }
Graceful Shutdown
애플리케이션이 종료될 때 진행 중인 요청을 완료한 후 종료합니다.
server:
shutdown: graceful # 기본: immediate
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 최대 대기 시간
종료 순서:
- 새 요청 수신 중단
- 진행 중인 요청 완료 대기
@PreDestroy실행- 빈 소멸
- ApplicationContext 종료
주의할 점
1. lazy-initialization을 켜면 첫 요청에서 타임아웃이 발생할 수 있다
spring.main.lazy-initialization=true로 설정하면 시작 시간은 줄어들지만, 빈 생성이 첫 요청 시점으로 지연됩니다. DB 커넥션 풀 초기화, 캐시 워밍업 등이 모두 첫 요청에서 발생하여 응답 시간이 수 초 이상 걸릴 수 있습니다. 특히 헬스체크가 성공한 직후 트래픽을 받으면 첫 사용자들이 타임아웃을 경험합니다.
2. CommandLineRunner에서 예외가 발생하면 애플리케이션 전체가 종료된다
CommandLineRunner나 ApplicationRunner의 run() 메서드에서 예외가 발생하면, 스프링부트가 이를 시작 실패로 간주하고 전체 애플리케이션을 종료합니다. 초기 데이터 로드나 외부 서비스 연결 확인에서 일시적 장애가 나면 서비스가 아예 뜨지 않습니다. 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으로 시작 단계별 소요 시간을 확인할 수 있습니다.