학습 자료

@SpringBootApplication — 마법이 아니라 기본값이다


Article

4강에서 두 가지가 이상했다.

build.gradlespring-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 들을 열어 그 파일을 세어봤다.

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 의 다섯 줄을 열어보면 이렇게 생겼다.

spring-boot-tomcat-4.1.0.jar 안의 imports
  • ...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)

리포트는 네 부분이다.

CONDITIONS EVALUATION REPORT
섹션우리 프로젝트
Positive matches조건을 만족해서 켠 것52 항목
Negative matches조건을 못 채워서 안 켠 것39 항목
Exclusions우리가 명시적으로 뺀 것없음
Unconditional classes조건 없이 무조건 켜는 것6개
후보 클래스는 30개인데 항목이 91개인 이유는, 클래스 안의 중첩 설정과 @Bean 메서드까지 하나하나 따로 판정하기 때문이다.

조건은 몇 종류 없다

리포트에 등장한 조건들을 세어봤다.

리포트에 등장한 @ConditionalOn... 횟수
@ConditionalOnMissingBean35회
@ConditionalOnClass34회
@ConditionalOnBooleanProperty10회
@ConditionalOnBean9회
@ConditionalOnThreading5회
@ConditionalOnProperty4회
@ConditionalOnWebApplication1회
이름을 소리 내어 읽으면 뜻이 그대로 나온다. "빈이 없으면", "클래스가 있으면", "속성이 켜져 있으면".

가장 많이 쓰인 것이 @ConditionalOnMissingBean 이다. "그 빈이 없으면 내가 만든다."

이 한 줄이 자동설정의 정체다. Spring 은 우리를 앞지르지 않는다. 우리가 안 만들었을 때만 만든다.

두 번째가 @ConditionalOnClass 다. "그 클래스가 클래스패스에 있으면." build.gradle 에 톰캣이 딸려 왔으니 톰캣 클래스가 있고, 그러니 톰캣 자동설정을 켠다. 4강의 그 한 줄이 여기까지 이어진다.

톰캣은 왜 켜졌나

리포트에서 톰캣 항목을 찾아보자.

Positive matches 안에서
  • 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#tomcatServletWebServerFactory matched:
  • @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 beansfound 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 자신도 빈이었다.

@SpringBootApplication 한 줄이 하는 일
  1. @ComponentScan내 패키지 아래에서 @Component 를 찾는다 (7강)
  2. @EnableAutoConfiguration후보 30개를 조건에 걸어 켠다
  3. @SpringBootConfiguration여기에 @Bean 을 적을 수 있다
146개의 빈은 이 셋이 만든 것이다. 우리 것 넷과, 자동설정이 만든 나머지.

정리하면

자동설정은 목록에 적힌 후보를 조건에 걸어 켜는 일이고, 그 조건의 대부분은 "우리가 안 만들었으면"이다.

다시 짚어보기
  1. 01

    후보 목록은 jar 안의 텍스트 파일이다

    우리 프로젝트는 jar 여섯 개에서 30줄. 의존성을 넣으면 후보가 함께 들어온다.

    META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  2. 02

    켤지 말지는 @ConditionalOn... 이 정한다

    가장 많이 쓰이는 것은 @ConditionalOnMissingBean — 그 빈이 없으면 내가 만든다.

  3. 03

    우리가 만들면 Spring 이 물러선다

    @Bean 하나로 톰캣을 9091 에 띄웠다. 리포트의 그 항목이 Positive 에서 Negative 로 내려갔다.

    did not find any beans   →   found beans of type '...'
  4. 04

    설정 파일의 값도 빈이 된다

    server.port 는 우리가 지어낸 이름이 아니라 TomcatServerProperties 의 필드다.

  5. 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 이 부르기 때문이다.

@SpringBootApplication — 마법이 아니라 기본값이다 — 디코드랩(DCODELAB)