자바 백엔드 입문 보강편. 12편에서 만든 첫 프로젝트가 설정 한 줄 없이 톰캣을 띄우고 DB에 붙는 그 '알아서 됨'의 정체 — Spring Boot 자동 구성을 @SpringBootApplication부터 @ConditionalOnClass·AutoConfiguration.imports·디버깅까지 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈의 보강편이에요. 12편에서 start.spring.io로 첫 프로젝트를 만들고 ▶ Run 한 번에 톰캣이 8080에 떴죠. 그때 슬쩍 넘어간 게 하나 있어요. 나는 설정 파일에 아무것도 안 썼는데, 톰캣은 어떻게 떴고 JSON 응답은 누가 만들어 줬을까. 그 "알아서 됨"의 정체가 바로 Spring Boot의 자동 구성(Auto Configuration)입니다. 12편과 13편 application.yml 사이에 놓고 읽으면 딱 맞아요.
자동 구성이 처음엔 마법처럼 느껴지는 이유
처음 Spring Boot를 만지면 이 부분이 제일 얼떨떨해요. 이유는 두 가지예요.
첫째, 내가 안 만든 Bean이 어디서 왔는지가 안 보여요. 옛날 Spring은 톰캣 연결, DataSource, 메시지 컨버터를 XML에 일일이 적었거든요. Boot는 그걸 다 지워버렸는데, 지운 설정이 "사라진" 게 아니라 "보이지 않는 곳에서 자동으로 채워진" 거라 더 헷갈려요.
둘째, "그럼 내가 직접 설정하면 충돌 나는 거 아냐?" 라는 불안. 결론부터 말하면 안 나요. 자동 구성은 항상 내가 만든 걸 먼저 존중하거든요. 이게 핵심인데, 뒤에서 @ConditionalOnMissingBean으로 풀게요.
비유 한 줄 — 자동 구성은 신입사원 책상 세팅 같은 거예요. 입사하면(=의존성을 추가하면) 총무팀이 알아서 책상·노트북·사번을 깔아 둬요. 단, 내가 노트북을 직접 들고 왔으면 총무팀은 그건 안 건드리고 비켜 줍니다. 이 글은 그 "총무팀"이 정확히 누구이고 무슨 규칙으로 움직이는지를 풀어 가요.
@SpringBootApplication 한 줄에 숨은 세 가지
12편 메인 클래스 기억나죠. 딱 이 한 줄이 출발점이에요.
@SpringBootApplication
public class MyShopApplication {
public static void main(String[] args) {
SpringApplication.run(MyShopApplication.class, args);
}
}
@SpringBootApplication은 사실 어노테이션 세 개를 합친 합성 어노테이션이에요. 소스를 열어 보면 이렇게 생겼어요.
@SpringBootConfiguration // 사실상 @Configuration — 이 클래스도 설정 클래스
@ComponentScan // 이 패키지부터 아래로 @Component 다 찾아 등록
@EnableAutoConfiguration // 나머지는 알아서 채워 줘
public @interface SpringBootApplication { ... }
19편 @Configuration·@Bean과 18편 @Component·@Autowired에서 다룬 게 앞의 둘이에요. @ComponentScan은 내가 짠 코드(컨트롤러·서비스·리포지토리)를 훑어 컨테이너에 올리는 역할이고요. 그러니까 내 코드를 등록하는 건 @ComponentScan, 나머지 인프라를 채우는 건 @EnableAutoConfiguration — 이 분업이 자동 구성을 이해하는 첫 갈림길이에요.
여기서 시험 함정이 하나 있어요. @ComponentScan은 메인 클래스가 놓인 패키지부터 그 하위만 훑어요. 그래서 컨트롤러를 메인 클래스보다 바깥 패키지에 두면 "분명 @RestController 붙였는데 404가 뜨는" 상황이 생깁니다. 자동 구성 문제가 아니라 스캔 범위 문제예요.
자동 구성 후보는 어디서 오나 — AutoConfiguration.imports
@EnableAutoConfiguration이 켜지면 Boot가 가장 먼저 하는 일은 후보 목록 읽기예요. 클래스패스에 깔린 모든 라이브러리(jar) 안을 뒤져서 이 파일을 찾아요.
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
이름이 길죠. 예전 Boot 2.6 이하에서는 META-INF/spring.factories라는 파일에 적혀 있었는데, 2.7부터 위 .imports 파일로 바뀌었어요. 둘 다 하는 일은 똑같아요 — "이 jar에는 이런 자동 구성 클래스들이 들어 있습니다" 라고 적어 둔 명단이에요. 한 줄에 클래스 하나씩.
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
... (spring-boot-autoconfigure jar 하나에만 수백 개)
Boot는 클래스패스에 있는 모든 jar의 이 명단을 싹 모아요. 그러면 후보 자동 구성 클래스가 수백 개 쌓여요.
여기까지 따라오셨다면 한 가지 의문이 들 거예요. "후보가 수백 개면, 그게 다 적용돼서 안 쓰는 Bean까지 잔뜩 생기는 거 아냐?" 아니에요. 후보일 뿐이고, 실제로 켜지는 건 조건을 통과한 것만입니다. 그 조건이 다음 이야기예요.
@Conditional — 조건이 맞을 때만 켜진다
자동 구성 클래스 하나하나에는 "이런 상황일 때만 나를 적용해" 라는 조건표가 붙어 있어요. 이게 @Conditional 계열 어노테이션이에요. 자주 보는 둘만 짚으면 충분해요.
@ConditionalOnClass(...)— 클래스패스에 특정 클래스가 있을 때만 켜져요. 그 라이브러리를 안 썼으면 조용히 건너뜀.@ConditionalOnMissingBean(...)— 같은 타입의 Bean을 내가 직접 안 만들었을 때만 켜져요. 내가 만들었으면 비켜 줌.
실제 DataSourceAutoConfiguration을 단순화하면 이런 모양이에요.
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // JDBC 의존성이 있을 때만
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 내가 DataSource를 안 만들었을 때만
public DataSource dataSource(...) {
// HikariCP 같은 기본 커넥션 풀로 DataSource 생성
}
}
읽어 보면 규칙이 그대로 보여요. JDBC 관련 클래스가 클래스패스에 있고(=spring-boot-starter-data-jpa 같은 걸 추가했고), 내가 DataSource를 직접 정의하지 않았을 때만 Boot가 기본 DataSource를 만들어 줘요. 둘 중 하나라도 어긋나면 이 자동 구성은 켜지지 않아요.
@ConditionalOnMissingBean 한 줄이 "내가 만들면 내 게 이긴다"는 규칙의 정체예요. 자동 구성은 언제나 사용자 정의에 양보합니다. 그래서 첫 단락의 "내가 직접 설정하면 충돌 나나?" 걱정은 안 해도 돼요 — Boot가 알아서 물러나요.
스타터 하나가 자동 구성을 통째로 켠다
그럼 클래스패스에 라이브러리를 어떻게 깔길래 조건이 맞춰지는 걸까요. 답이 스타터(starter)예요. spring-boot-starter-web 한 줄을 build.gradle에 적으면, 그 안에 톰캣·Spring MVC·Jackson이 한 묶음으로 딸려 와요. 라이브러리가 깔리니까 @ConditionalOnClass 조건이 줄줄이 만족되고, 관련 자동 구성이 도미노처럼 켜지는 거예요.
스타터는 직접 코드를 갖고 있지 않아요. "이 기능을 쓰려면 필요한 라이브러리들" 을 한데 묶어 둔 의존성 꾸러미일 뿐이에요. 그래서 스타터 하나 추가 = "이 기능 쓸 거니까 관련 자동 구성 다 켜 줘"라는 신호가 됩니다.
| 스타터 | 깔리는 것 | 자동으로 켜지는 자동 구성(예시) |
|---|---|---|
spring-boot-starter-web | 톰캣, Spring MVC, Jackson | DispatcherServlet·내장 톰캣·JSON 메시지 컨버터 |
spring-boot-starter-data-jpa | Hibernate, Spring Data JPA | DataSource·EntityManager·트랜잭션 매니저 |
spring-boot-starter-security | Spring Security | 기본 로그인 폼·시큐리티 필터 체인 |
12편에서 의존성으로 골랐던 Spring Web이 바로 이 starter-web이에요. 그거 하나 체크한 덕분에 톰캣이 8080에 떴던 거죠. 마법이 아니라 조건이 맞아서 켜진 자동 구성이었어요.
자동 구성을 눈으로 확인하는 법
여기까지 읽고 나면 자연스레 궁금해져요. "그래서 지금 내 앱에서 뭐가 켜졌고 뭐가 안 켜졌는데?" Boot는 이걸 보여주는 리포트를 갖고 있어요. application.yml에 한 줄만 켜면 돼요.
debug: true
또는 실행할 때 --debug 플래그를 붙여도 같아요.
java -jar my-shop.jar --debug
그러면 부팅 로그에 ConditionEvaluationReport(조건 평가 리포트)가 찍혀요. 모양이 이래요.
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches: ← 조건을 통과해 "켜진" 자동 구성
-----------------
DispatcherServletAutoConfiguration matched:
- @ConditionalOnClass found required class 'DispatcherServlet'
Negative matches: ← 조건이 안 맞아 "꺼진" 자동 구성
-----------------
DataSourceAutoConfiguration:
- @ConditionalOnClass did not find required class 'javax.sql.DataSource'
Positive matches는 켜진 것, Negative matches는 안 켜진 것과 왜 안 켜졌는지 이유까지 보여줘요. "DB 설정을 했는데 DataSource가 안 만들어진다" 싶을 때 이 리포트를 보면 "JDBC 의존성을 안 넣어서 조건 자체가 불발됐다" 가 한눈에 잡혀요. 디버깅의 출발점이에요.
57편 Actuator를 붙였다면 /actuator/conditions 엔드포인트로 같은 정보를 JSON으로도 받을 수 있어요. 로그를 뒤지는 것보다 깔끔하죠.
자주 막히는 함정
입문자가 자동 구성 때문에 실제로 자주 멈추는 지점 세 개만 정리할게요.
컴포넌트가 안 잡혀요. 90%는 메인 클래스보다 바깥 패키지에 클래스를 둔 경우예요. @ComponentScan은 메인 클래스 패키지 하위만 본다는 걸 떠올리면 돼요. 메인 클래스를 최상위 패키지에 두는 게 안전합니다.
특정 자동 구성을 끄고 싶어요. 기본 DataSource 같은 걸 내 손으로만 관리하고 싶을 때가 있어요. 그럴 땐 명시적으로 제외해요.
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class MyShopApplication { ... }
DataSource를 직접 만들었더니 둘이 충돌하는 것 같아요. 사실 충돌은 안 나요. 내가 만든 순간 @ConditionalOnMissingBean이 자동 구성을 꺼버리니까. 다만 DataSource를 여러 개 직접 만들면 Boot가 "어느 걸 기본으로?" 하고 헷갈려해요. 그땐 하나에 @Primary를 붙여 주면 됩니다.
시험·면접 직전 압축 노트 — 자동 구성
- 자동 구성 = "설정 안 써도 Boot가 알아서 인프라 Bean을 채워 주는" 메커니즘
- 시작점은
@SpringBootApplication한 줄 @SpringBootApplication=@SpringBootConfiguration+@ComponentScan+@EnableAutoConfiguration@ComponentScan= 내 코드(컨트롤러·서비스) 등록 /@EnableAutoConfiguration= 나머지 인프라 채움@ComponentScan은 메인 클래스 패키지 하위만 스캔 — 바깥에 두면 안 잡힘- 자동 구성 후보 명단 = jar 안의
META-INF/spring/...AutoConfiguration.imports - Boot 2.7 이전엔
META-INF/spring.factories에 적혀 있었음 (하는 일은 동일) - 후보는 수백 개지만 조건을 통과한 것만 실제로 켜짐
@ConditionalOnClass= 클래스패스에 특정 클래스 있을 때만 켜짐@ConditionalOnMissingBean= 내가 같은 Bean을 안 만들었을 때만 켜짐- 핵심 한 줄 — 내가 만들면 내 Bean이 이긴다 (자동 구성은 사용자 정의에 양보)
- 스타터(starter) = 코드가 아니라 의존성 묶음 — 라이브러리를 깔아 조건을 충족시킴
starter-web하나가 톰캣·MVC·Jackson 자동 구성을 통째로 켬- 켜진/꺼진 자동 구성 확인 =
application.yml에debug: true또는--debug - 리포트의 Positive matches = 켜진 것 / Negative matches = 안 켜진 것 + 이유
- Actuator 붙이면
/actuator/conditions로 같은 정보 JSON 확인 가능 - 특정 자동 구성 끄기 =
@SpringBootApplication(exclude = { ... }) - DataSource 여러 개 직접 정의 시 하나에
@Primary지정
공식 문서: Spring Boot — Auto-configuration과 Condition Annotations에서 더 깊은 사양을 확인할 수 있어요.
시리즈 다른 편
개념상 이 자리 (12편과 13편 사이):
- 12편 — start.spring.io 첫 프로젝트
- 보강 — Spring Boot 자동 구성 (현재 글)
- 13편 — application.yml·Spring Profiles
같이 읽으면 좋은 글: