@SpringBootApplication — 마법이 아니라 기본값이다
Article
4강에서 두 가지가 이상했다.
build.gradle 에 spring-boot-starter-webmvc 한 줄을 적었을 뿐인데 톰캣이 떴다. server.port 라는 이름을 우리가 정한 적이 없는데 Spring 이 알고 있었다. 5강에서 상자를 열어보니 빈이 146개 들어 있었다.
7강에서 @SpringBootApplication 을 뜯어보니 표식 셋이 나왔다. 그중 @ComponentScan 은 풀었다. 남은 둘이다.
| 표식 | 무엇을 하나 |
|---|---|
@EnableAutoConfiguration | 안 정한 것들의 기본값을 채운다 |
@SpringBootConfiguration | 설정 클래스라는 표시 |
이번 강의에서 그 146개가 어디서 왔는지 본다. 미리 말해두면, 마법이 아니다. 목록이 파일에 적혀 있고, 켤지 말지는 if 문으로 정해진다.
후보 목록은 파일에 적혀 있다
Spring Boot 는 "무엇을 자동으로 설정할 수 있는지"를 텍스트 파일에 적어둔다. jar 안에 들어 있다.
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
우리 프로젝트가 실제로 쓰는 jar 들을 열어 그 파일을 세어봤다.
- spring-boot-autoconfigure-4.1.0.jar 12
- spring-boot-webmvc-4.1.0.jar 6
- spring-boot-tomcat-4.1.0.jar 5
- spring-boot-servlet-4.1.0.jar 5
- spring-boot-http-converter-4.1.0.jar 1
- spring-boot-jackson-4.1.0.jar 1
- ------------------------------------------------
- 합계 30
후보는 30개다. 그중 톰캣 jar 의 다섯 줄을 열어보면 이렇게 생겼다.
- ...tomcat.autoconfigure.actuate.web.server.TomcatReactiveManagementContextAutoConfiguration
- ...tomcat.autoconfigure.actuate.web.server.TomcatServletManagementContextAutoConfiguration
- ...tomcat.autoconfigure.metrics.TomcatMetricsAutoConfiguration
- ...tomcat.autoconfigure.reactive.TomcatReactiveWebServerAutoConfiguration
- ...tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration
마지막 줄이 4강에서 톰캣을 띄운 그 클래스다.
4강에서 starter 는 묶음이라고 했다. 이제 그 말의 뜻이 분명해진다. spring-boot-starter-webmvc 를 적으면 톰캣 jar 이 딸려 오고, 그 jar 안에 자동설정 후보 목록이 함께 들어온다. 의존성을 추가하는 것은 곧 자동설정 후보를 추가하는 것이다.
후보 30개 중 무엇을 켜나
후보라고 해서 다 켜지는 게 아니다. TomcatReactiveWebServerAutoConfiguration 은 우리가 안 쓴다. 리액티브 웹이 아니니까.
켤지 말지를 조건으로 판단한다. 그 판단 과정을 Spring 이 통째로 보여준다.
./gradlew bootRun --args='--debug'
- ============================
- CONDITIONS EVALUATION REPORT
- ============================
- Positive matches:
- -----------------
- AopAutoConfiguration matched:
- - @ConditionalOnBooleanProperty (spring.aop.auto=true) matched (OnPropertyCondition)
리포트는 네 부분이다.
| 섹션 | 뜻 | 우리 프로젝트 |
|---|---|---|
Positive matches | 조건을 만족해서 켠 것 | 52 항목 |
Negative matches | 조건을 못 채워서 안 켠 것 | 39 항목 |
Exclusions | 우리가 명시적으로 뺀 것 | 없음 |
Unconditional classes | 조건 없이 무조건 켜는 것 | 6개 |
조건은 몇 종류 없다
리포트에 등장한 조건들을 세어봤다.
가장 많이 쓰인 것이 @ConditionalOnMissingBean 이다. "그 빈이 없으면 내가 만든다."
이 한 줄이 자동설정의 정체다. Spring 은 우리를 앞지르지 않는다. 우리가 안 만들었을 때만 만든다.
두 번째가 @ConditionalOnClass 다. "그 클래스가 클래스패스에 있으면." build.gradle 에 톰캣이 딸려 왔으니 톰캣 클래스가 있고, 그러니 톰캣 자동설정을 켠다. 4강의 그 한 줄이 여기까지 이어진다.
톰캣은 왜 켜졌나
리포트에서 톰캣 항목을 찾아보자.
- TomcatServletWebServerAutoConfiguration#tomcatServletWebServerFactory matched:
- - @ConditionalOnMissingBean (types: ...ServletWebServerFactory; SearchStrategy: current)
- did not find any beans (OnBeanCondition)
did not find any beans. 찾아봤는데 없더라, 그래서 내가 만들었다는 뜻이다.
우리가 new ServerSocket(8080) 을 안 쳤는데 8080 이 열린 이유가 이것이다. 1강부터 끌고 온 질문이 여기서 끝난다.
그러면 우리가 만들면 어떻게 되나
조건이 @ConditionalOnMissingBean 이라면, 빈이 있으면 안 만들어야 한다. 정말 그런지 확인해보자.
@Bean 을 붙인 메서드를 하나 만든다. 이 메서드가 돌려주는 객체가 빈이 된다.
@SpringBootApplication
public class BookstoreApplication {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setPort(9091);
return factory;
}
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}
포트를 9091 로 박아뒀다. Spring 이 물러선다면 우리 것이 쓰일 것이고, 안 물러선다면 8080 이 뜰 것이다.
- INFO --- Tomcat started on port 9091 (http) with context path '/'
- INFO --- Started BookstoreApplication in 1.493 seconds
물러섰다. 그리고 리포트에서 그 항목을 다시 찾아보면, Positive matches 에 없다. Negative matches 로 옮겨가 있다.
우리가 안 만들었을 때
Positive matches
TomcatServletWebServerAutoConfiguration#tomcatServletWebServerFactorymatched:@ConditionalOnMissingBean (types: ServletWebServerFactory)- `did not find any beans`
- → Spring 이 만든다. 8080
우리가 만들었을 때
Negative matches
TomcatServletWebServerAutoConfiguration#tomcatServletWebServerFactory:- `Did not match:`
- `found beans of type '...ServletWebServerFactory' tomcatServletWebServerFactory`
- → 물러선다. 9091
did not find any beans 가 found beans of type ... 로 바뀌었다. 문장 하나가 뒤집혔을 뿐이다.
server.port 는 어디서 왔나
물론 톰캣 팩토리를 손으로 만들자고 포트를 바꾸는 사람은 없다. 4강에서 우리는 이렇게 했다.
server.port=9090
5강에서 빈 목록을 훑을 때 이런 이름이 지나갔다.
server.tomcat-org.springframework.boot.tomcat.autoconfigure.TomcatServerProperties
설정 파일의 값도 빈이 된다. server. 로 시작하는 속성들을 읽어 객체에 담아두고, 톰캣 자동설정이 그 객체를 참고해 포트를 정한다. 우리가 server.port 라는 이름을 지어낸 게 아니라, 그 객체에 port 라는 필드가 있는 것이다.
마지막 표식
@SpringBootConfiguration 이 남았다.
이건 "이 클래스는 설정 클래스다"라는 표시다. 설정 클래스 안에서는 @Bean 메서드를 쓸 수 있다. 방금 우리가 BookstoreApplication 안에 @Bean 을 넣을 수 있었던 이유가 이것이다.
7강에서 표식을 타고 올라가 봤다. @SpringBootConfiguration → @Configuration → @Component. 그래서 BookstoreApplication 자신도 빈이었다.
- @ComponentScan내 패키지 아래에서 @Component 를 찾는다 (7강)
- @EnableAutoConfiguration후보 30개를 조건에 걸어 켠다
- @SpringBootConfiguration여기에 @Bean 을 적을 수 있다
정리하면
자동설정은 목록에 적힌 후보를 조건에 걸어 켜는 일이고, 그 조건의 대부분은 "우리가 안 만들었으면"이다.
- 01
후보 목록은 jar 안의 텍스트 파일이다
우리 프로젝트는 jar 여섯 개에서 30줄. 의존성을 넣으면 후보가 함께 들어온다.
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - 02
켤지 말지는
@ConditionalOn...이 정한다가장 많이 쓰이는 것은
@ConditionalOnMissingBean— 그 빈이 없으면 내가 만든다. - 03
우리가 만들면 Spring 이 물러선다
@Bean하나로 톰캣을 9091 에 띄웠다. 리포트의 그 항목이 Positive 에서 Negative 로 내려갔다.did not find any beans → found beans of type '...' - 04
설정 파일의 값도 빈이 된다
server.port는 우리가 지어낸 이름이 아니라TomcatServerProperties의 필드다. - 05
--debug로 판정 과정을 통째로 볼 수 있다빈이 안 생길 때 Negative matches 에서 이름을 찾으면 사유가 적혀 있다.
Part 2 가 끝났다.
5강에서 상자를 열었고, 6강에서 생성자에 타입만 적으면 컨테이너가 넣어준다는 것을 봤고, 7강에서 그 표식을 찾아내는 과정이 자바 편 21강의 리플렉션이었음을 확인했고, 8강에서 나머지 146개가 어디서 왔는지 밝혔다.
이제 new 가 사라진 이유를 전부 안다. 자바 편은 new 로 객체를 만드는 법까지였고, 여기까지가 new 를 쓰지 않게 되는 이유였다.
그런데 아직 우리 서버는 아무 일도 하지 않는다. 4강에서 띄운 뒤로 어떤 주소로 요청해도 404 다.
3강에서 배운 약속을 기억하는지. 경로는 명사, 메서드는 동사, 상태코드는 결과. 그리고 자바 편 21강의 마지막 페이지에 설명 없이 놓여 있던 그 코드.
@RestController
class BookController { ... }
9강에서 저 표식을 붙인다. 그리고 main 에 마지막으로 남아 있던 getBean 도 그때 사라진다. 컨트롤러는 우리가 부르는 게 아니라, 요청이 들어올 때 Spring 이 부르기 때문이다.