main 메서드는 한 줄인데, 실행하면 로그가 쏟아지고 서버가 떠요.
처음 생성한 Spring Boot 프로젝트에는 보통 이런 클래스가 있어요.
curl로 요청을 보내면 컨트롤러가 응답해요. 내가 직접 웹 서버를 만든 적도 없고, 컨트롤러 객체를 만든 적도 없는데요.
그래서 오늘 질문은 이거예요.
“SpringApplication.run(...) 한 줄 안에서 무슨 일이 벌어지길래 앱이 살아나는 걸까요?”
이 글은 내부 구현의 모든 메서드를 따라가는 글은 아니에요. 목표는 실행 로그를 볼 때 “아, 지금 환경을 준비하는 중이구나”, “컨테이너가 빈(bean)을 만드는 중이구나”, “이 시점에 웹 서버가 붙는구나” 정도의 큰 흐름을 잡는 거예요.
예시는 Spring Boot 4.x 흐름을 기준으로 설명해요.
SpringApplication, @SpringBootApplication, 자동 설정(auto-configuration), 웹 애플리케이션 타입 같은 표현은 Spring Boot 4.1.0 공식 문서를 확인해 작성했어요.Java가 먼저 부르는 곳은 여전히 main이에요
Spring Boot를 써도 Java 프로그램의 출발점은 그대로예요.
JVM은 public static void main(String[] args)를 찾아 실행해요. 여기까지는 평범한 Java 애플리케이션과 같아요.
차이는 그다음 줄이에요.
OrderApplication.class는 단순히 “이 클래스를 객체로 만들어줘”라는 뜻이 아니에요. 이 클래스에 붙은 @SpringBootApplication을 보고, Spring Boot가 어디서부터 설정과 컴포넌트를 찾을지 판단하는 기준점이 돼요.
앞 글에서 본 IoC, DI, AOP는 이 순간부터 실제 실행 흐름으로 들어와요. 컨테이너가 만들어지고, 빈이 등록되고, 필요한 객체들이 연결되고, 프록시(proxy)가 필요한 곳에는 프록시도 준비될 수 있어요.
@SpringBootApplication은 세 가지 신호를 한 번에 줘요
OrderApplication 위에는 @SpringBootApplication이 붙어 있어요.
여기서 초보자가 가장 자주 헷갈리는 부분은 컴포넌트 스캔이에요.
예를 들어 시작 클래스가
com.example.order 패키지에 있으면, 보통 그 아래 패키지들이 탐색 범위가 돼요.
com.example.order
OrderApplication.java
controller
OrderController.java
service
OrderService.java
repository
OrderRepository.java
controller, service, repository가 시작 클래스 아래에 있으니 자연스럽게 발견될 수 있어요.
반대로 이런 구조라면 초반부터 이상한 일이 생길 수 있어요.
com.example.order
OrderApplication.java
com.example.payment
PaymentService.java
PaymentService가 시작 클래스의 하위 패키지 밖에 있으면, 기대와 달리 스캔되지 않을 수 있어요. 그래서 프로젝트를 만들 때 패키지 이름을 대충 잡으면 나중에 “왜 이 클래스는 빈으로 안 잡히지?”라는 질문으로 돌아와요.
실행은 대략 이런 순서로 지나가요
SpringApplication.run(...) 내부는 꽤 많은 일을 해요. 처음에는 아래 정도의 흐름으로 잡으면 충분해요.
앱을 켜는 과정은 스위치 하나를 올리는 장면보다 앞 단계의 결과를 다음 단계가 넘겨받는 준비 작업에 가까워요. 시작 클래스와 실행 인자가 환경으로 넘어가고, 환경은 컨텍스트가 어떤 빈과 서버를 준비할지 판단하는 재료가 돼요.
1
JVM이 main 메서드를 호출해요
JVM이 Java 프로그램의 진입점인
main(String[] args)를 찾아 실행해요. 아직 Spring 컨테이너도, 빈도, 웹 서버도 없는 평범한 Java 실행 단계예요.2
main이 시작 정보를 Boot에 넘겨요
OrderApplication.class는 어디서 설정과 컴포넌트를 찾기 시작할지 알려주는 주 설정 소스(primary source)예요. args에는 --spring.profiles.active=local 같은 실행 인자가 들어올 수 있어요. SpringApplication.run(...)은 이 두 재료를 넘겨받아 환경과 컨텍스트를 준비하고, 실행이 끝나면 준비된 ApplicationContext를 돌려줘요.3
실행 환경을 조립해요
Spring Boot가 프로필, 설정 파일, 환경 변수, 명령줄 인자를 모아요. 뒤 단계는 이 값을 보고 설정을 정해요.
4
ApplicationContext를 만들어요
준비한 환경과 애플리케이션 종류에 맞춰 ApplicationContext를 만들어요. 이 컨테이너에는 아직 우리가 쓸 빈이 준비되지 않았어요.
5
빈 정의와 자동 설정을 모아요
컴포넌트 스캔으로 내
@Controller, @Service, @Repository를 찾아요. 프로젝트 의존성과 기존 빈을 보고 자동 설정도 골라요. 이 정보가 빈 정의로 먼저 쌓여요. 실제 객체는 아직 없어요.6
refresh로 실제 빈을 준비해요
컨텍스트를 새로고침(refresh)하며 빈을 만들고 의존성을 연결해요. 초기화 콜백과 빈 후처리기도 이어서 동작해요. 필요한 프록시도 이때 생겨요.
7
웹 애플리케이션이면 서버를 열어요
웹 애플리케이션이면 refresh 중에 내장 웹 서버도 시작해요. 컨트롤러는 포트를 열지 않아요. 웹 서버와 Spring MVC가 요청을 전달할 구조를 준비해요.
8
시작 완료를 알리고 러너를 실행해요
컨텍스트 준비가 끝나면
Started OrderApplication ... 로그가 보여요. 이어서 ApplicationRunner와 CommandLineRunner를 호출해요. 이 작업이 끝나면 Boot가 준비 완료 상태를 알려요.main과 ApplicationContext 사이예요. main은 Spring Boot를 호출하는 Java의 입구예요. 실제 애플리케이션 객체를 만들고 연결하는 일은 그 뒤에 준비된 ApplicationContext가 맡아요.
또 하나 기억할 경계가 있어요. 웹 서버의 포트가 열렸다고 시작 작업이 모두 끝난 것은 아니에요. 러너에서 예외가 나면 애플리케이션은 준비 완료 상태에 도달하지 못할 수 있어요. 그래서 운영 환경에서는 단순히 프로세스가 살아 있는지와 실제 요청을 받을 준비가 됐는지를 나눠서 봐요.
이제 각 단계를 조금만 더 풀어볼게요.
먼저 환경을 준비해요
애플리케이션이 실행되면 Spring Boot는 먼저 실행 환경을 잡아요. 여기서 말하는 환경은 “내 노트북이냐 서버냐”만 뜻하지 않아요. Spring이 설정값을 읽고 판단하는 재료 전체에 가까워요.
예를 들어 이렇게 실행할 수 있어요.
8081을 사용할 수 있어요. 정확히 어떤 설정이 어느 설정을 이기는지는 뒤의 설정 글에서 따로 다룰 거예요.
지금은 한 가지만 잡으면 돼요.
SpringApplication.run은 빈을 만들기 전에, 어떤 설정으로 앱을 시작할지부터 준비해요.
왜 먼저 준비해야 할까요? 어떤 빈은 프로필에 따라 만들어질 수도 있고, 안 만들어질 수도 있어요. 웹 서버 포트처럼 실행 모양을 바꾸는 값도 컨텍스트가 본격적으로 준비되기 전에 알아야 해요.
그다음 ApplicationContext를 만들어요
환경이 준비되면 Spring Boot는 애플리케이션 컨텍스트(application context)를 만들어요. 애플리케이션 컨텍스트는 Spring 컨테이너의 대표적인 형태예요. 여기에는 빈 정의, 실제 빈 객체, 설정, 이벤트 발행 기능 같은 것들이 모여요. 조금 거칠게 말하면 이래요.
여기서 “등록”과 “생성”을 구분하면 좋아요.
먼저 Spring은 어떤 빈을 만들 수 있는지 정의를 모아요. 그다음 컨텍스트를 새로고침(refresh)하는 과정에서 실제 객체를 만들고, 필요한 의존성을 넣고, 초기화 과정을 거쳐요.
1
클래스와 설정 읽기
Spring은 시작 클래스의
@SpringBootApplication, 직접 작성한 @Configuration, 그리고 가져온 설정을 읽어요. 이때 컴포넌트 스캔과 자동 설정을 어디서 시작할지 결정해요.2
빈 정의 등록
@Controller, @Service, @Repository, @Component, @Bean처럼 컨테이너가 관리할 대상을 빈 정의로 기록해요. 아직 모든 객체를 만든 것이 아니라, 무엇을 어떻게 만들지에 대한 설계도를 모으는 단계예요.3
빈 객체 생성
컨텍스트가 빈 정의를 보고 실제 생성자를 호출해요. 생성자에
OrderRepository 같은 의존성이 필요하면 먼저 알맞은 빈을 찾고, 그 객체를 넣어 OrderService를 완성해요.4
초기화와 후처리
생성된 빈에 초기화 콜백과
BeanPostProcessor가 적용돼요. @Transactional처럼 프록시가 필요한 빈은 이 과정에서 원본 객체를 감싼 프록시로 노출될 수 있어요.5
사용 가능한 컨텍스트
필요한 빈이 생성되고 연결되면 ApplicationContext가 애플리케이션 객체 그래프를 제공할 수 있어요. 웹 애플리케이션은 이 컨텍스트를 바탕으로 컨트롤러까지 요청을 전달할 준비를 이어가요.
new OrderService()를 직접 쓰지 않아도 서비스 객체가 생겨요. 생성자에 필요한 객체가 있으면 컨테이너가 맞는 빈을 찾아 넣어줘요.
다음 글에서는 이 ApplicationContext와 빈(bean)을 더 자세히 볼 거예요. 오늘은 main에서 실행 중 앱으로 넘어가는 중간에 컨테이너가 만들어진다는 것만 붙잡으면 돼요.
자동 설정은 “비어 있는 자리”에 기본값을 채워요
Spring Boot의 자동 설정(auto-configuration)은 실행 중에 갑자기 아무거나 만드는 기능이 아니에요. 프로젝트에 어떤 라이브러리가 들어왔는지, 개발자가 이미 어떤 빈을 등록했는지, 어떤 설정값이 있는지를 보고 “이 조건이면 보통 이런 구성이 필요하겠네” 하고 기본 구성을 시도해요. 예를 들어 웹 스타터를 넣은 프로젝트라면 웹 요청을 처리할 준비가 필요해요.
여기서 핵심은 마지막 줄이에요.
자동 설정은 대체로 “개발자가 아무것도 못 바꾸게 덮어버리는 기능”이 아니라, 아직 명시하지 않은 자리에 기본값을 놓는 기능에 가까워요.
그래서 처음에는 편하게 시작하고, 필요해지면 직접 설정을 추가해 바꿔갈 수 있어요.
공식 문서는 어떤 자동 설정이 적용됐고 왜 적용됐는지 보고 싶을 때
--debug로 실행해 조건 평가 리포트를 확인할 수 있다고 안내해요. 처음부터 전부 읽을 필요는 없지만, “왜 이 설정이 들어왔지?”를 추적할 때 중요한 단서가 돼요.시작 실패는 보통 어느 단계에서 보일까요?
여기부터는 조금 더 깊게 볼게요.SpringApplication.run(...)의 순서를 아는 이유는 단순히 내부 흐름을 외우기 위해서가 아니에요. 앱이 안 뜰 때 어느 단계에서 실패했는지를 가늠하기 위해서예요.
Spring Boot 시작 실패는 겉으로는 모두 “앱이 안 켜짐”처럼 보이지만, 실제 원인은 서로 다른 층에 있을 수 있어요.
처음에는 에러 로그가 길어서 무섭게 느껴져요. 하지만 시작 흐름을 알고 있으면 로그를 이렇게 읽을 수 있어요.
“환경을 읽다가 실패했나?"Spring Boot의 실패 분석(failure analysis) 메시지는 이런 원인을 사람 말에 가깝게 풀어주려는 장치예요. 그래도 설명이 부족할 때는
"빈을 찾다가 실패했나?"
"빈은 만들었는데 초기화하다가 실패했나?"
"컨텍스트는 떴는데 웹 서버가 못 열렸나?”
--debug로 조건 평가 리포트를 보거나, Actuator를 쓰는 앱에서는 조건 정보를 확인해 자동 설정이 왜 들어왔는지 추적할 수 있어요.
여기서 중요한 습관은 에러를 “Spring Boot가 이상하다”로 뭉개지 않는 거예요. 시작 단계 중 어디에서 멈췄는지 찾으면, 봐야 할 파일과 질문이 줄어들어요.
웹 애플리케이션이면 웹 서버도 같이 준비돼요
Spring Boot 웹 프로젝트에서 특히 신기한 부분은 서버예요. 예전에는 Java 웹 애플리케이션을 별도 애플리케이션 서버에 올리는 방식이 익숙했어요. Spring Boot는 내장 서버를 함께 사용해서 애플리케이션 자체를 실행하는 흐름을 자연스럽게 만들어요. 그래서 웹 스타터가 들어 있고 웹 애플리케이션으로 판단되면, 컨텍스트가 준비되는 과정에서 웹 서버도 요청을 받을 준비를 해요. 이 그림에서 컨트롤러가 바로 서버를 여는 게 아니에요. 컨트롤러는 요청을 처리할 빈으로 준비되고, 웹 서버와 Spring MVC의 요청 처리 장치가 그 컨트롤러까지 요청을 전달해요. 만약 웹 서버가 필요 없는 배치성 프로그램이라면 설정으로 웹 애플리케이션 타입을 끌 수도 있어요.그럼 Started 로그는 언제 나오는 걸까요?
실행 로그의 마지막에 이런 줄을 자주 봐요.
ApplicationRunner나 CommandLineRunner 같은 실행 후 작업이 이어질 수 있어요.
예를 들어 앱이 뜬 직후 간단한 확인 작업을 하고 싶다면 이런 빈을 만들 수 있어요.
ApplicationRunner가 컨텍스트를 만드는 도구가 아니라는 거예요. 이미 컨텍스트가 준비된 뒤, 애플리케이션 시작 직후에 실행할 작업을 넣는 자리예요.
”내가 작성한 코드”와 “Boot가 준비한 일”을 나눠볼게요
처음으로 돌아가 볼게요.
이제 “main 한 줄이 마법처럼 서버를 띄웠다”보다 더 정확하게 말할 수 있어요.
main은 Spring Boot에게 시작 기준점과 실행 인자를 넘겼고, Spring Boot는 그 기준으로 환경, 컨테이너, 자동 설정, 웹 서버를 차례로 준비했어요.
자, 정리해볼까요?
-
Spring Boot 애플리케이션도 Java 프로그램이라서 시작점은
main메서드예요. -
SpringApplication.run(OrderApplication.class, args)는 시작 기준 클래스와 명령줄 인자를 받아 Spring 애플리케이션을 준비해요. -
@SpringBootApplication은 자동 설정(auto-configuration), 컴포넌트 스캔(component scan), 설정 클래스 역할을 함께 켜는 시작 신호예요. - 실행 중에는 환경 준비, ApplicationContext 생성, 빈 등록과 생성, 자동 설정 적용, 웹 서버 준비 같은 단계가 이어져요.
- 웹 서버는 컨트롤러가 직접 여는 것이 아니라, Spring Boot와 웹 관련 자동 설정이 준비한 실행 환경 위에서 요청을 받아요.
- 시작 실패를 읽을 때는 환경, 빈 등록, 의존성 주입, 초기화, 웹 서버 시작 중 어느 층에서 멈췄는지 나눠보면 좋아요.