Spring Boot 분석(구동 원리)
Spring Application 실행
아래의 모든 설명은 spring-boot 2.3.5 기준으로 설명합니다.
얼마전(2020/11월기준) GA 된 2.4 는 변경사항이 있습니다.
아래의 코드를 실행하면 Spring Application 이 수행된다. 나머지 정보들은 전혀 노출되지 않는데 어떻게 그 많은 정보들을 처리하게 되는지에 대한 내용을 정리한 글이다.
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
@SpringBootApplication 이 Key 인데 이는 아래와 같이 정의되어 있고 클래스 경로 설정 및 기타 Bean 및 다양한 속성 설정을 기반으로 Bean 추가를 시작하도록 하는 @EnableAutoConfiguration, 클래스가 Spring Boot Application @Configuration 을 제공함을 나타내는 @SpringBootConfiguration, @ComponentScan 같은 설정을 통해 Application 을 구성한다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// ,,,
}
기본 수행 항목
Spring Boot 는 SpringApplication 클래스를 통해 기본적으로 많은 구성을 하고 Spring application 을 실행시킵니다. 크게 이는 다음을 수행합니다.
-
적절한 ApplicationContext 인스턴스를 생성
-
CommandLinePropertySource 를 등록하여 argument 를 Spring 의 속성값으로 등록
-
모든 singleton Bean 을 로딩하고 ApplicationContext refresh
-
모든 CommandLineRunner Bean 을 트리거
이 때 코드안에 임의로 설정들을 선언하지 않고 spring-boot.jar 패키지안에 있는 META-INF/spring.factories 파일을 기반으로 구성됩니다. main 함수를 통해 실행되는 run()에서의 흐름의 순서는 아래와 같습니다. 각 흐름에 spring.factories 파일이 관여하여 인스턴스 생성을 위한 클래스명을 제공합니다.
SpringApplication.run() 의 상세 흐름
1. java.awt.headless 환경변수 설정 - GUI 클래스 사용에 대한 옵션(기본 true)
- GUI 가 필요없으면 -Djava.awt.headless=false 로 구성
2. SpringApplicationRunListeners 구성
- spring.factories 파일 기반으로 클래스들을 읽어서 Instance 로 등록하고 시작
- apache common logger 등록
3. CommanLine 을 통해 입력된 arguments 구성하고 Listener 와 더불어 구성 환경 및 Banner 준비
- active, default 의 프로파일을 구성하고 속성값 해석을 위한 준비 작업 수행
4. 위 환경 기반 ApplicationContext 를 생성하며 WebApplicationType 값으로 지정된 형식으로 context 를 구성하며 각 Type 에 따라 구성된 class 들을 인스턴스화 하고, 해당 context 에서 처리될 AnnotatedBean Reader/Scanner 등록, BeanFactory 구성등을 수행. 아래의 3가지 환경에 대한 Context 를 제공
- JSR 340 기반 Servlet 환경(org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext)
- Reactive Web WebFlux 환경(org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext)
- Web 이 아닌 일반 application 환경(org.springframework.context.annotation.AnnotationConfigApplicationContext)
5. 실행시 Spring Boot 의 구동 예외 현황에 대한 리포트를 위한 SpringBootExceptionReporter 구성
6. Context 에 환경변수를 설정하고 BeanFactory 를 통한 Singleton Bean 등록, ResourceLoader 설정, lazy 초기화구성 등의 초기화과정 수행하고 ApplicationContext 를 Refresh
7. log 시작 및 listeners 시작, CommandLineRunner 들을 호출하여 실행
spring.main.web-application-type
앞서 언급한 것처럼 ApplicationContext 구성에 WebApplicationType 이 사용된다.
해당 enum 을 통해 Application 의 형식을 지정할 수 있으며 이를 지정하여 사용하기 위해서는 application.yml(혹은 .properties) 를 통해 아래와 같이 선언하여 사용할 수 있다.
servlet 또는 reactive 를 사용하려면 그에 맞는 의존성을 추가하여야 하며 추가하지 않고 사용하면 당연하게도 아래와 같은 에러를 확인하게 된다.
maven 혹은 gradle 등의 의존성 관리도구를 사용하고 있다면 아래와 같이 의존성을 추가한다.
# gradle 을 통해 dependencies 선언
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.3.5.RELEASE'
}
그 외에 WebApplicationType 을 NONE 으로 지정한 경우 일반 Java Application 처럼 main thread 외에 별도로 작업을 위해 지속되는 thread 가 존재하지 않으면 바로 Application 은 종료된다.
Spring 은 어떻게 @(annotation) 들을 찾는가?
초기 spring 은 xml 을 통해 관련 정보들을 설정하고 해당 xml 을 application 에서 load 하여 실행하는 방식이였다. 개발 규모가 커지면서 이는 XML 파일들을 관리하기하는 또 하나의 요구사항이 생기게 되었고 이를 Annotation 사용으로 어느 정도 해결(이 또한 손이 가는건 어쩔 수 없다..)하였다.
이 역시 다른 환경설정들과 마찬가지로 SpringApplication.prepareContext() 에서 구현된 load() 메서드를 통해 Bean 등을 처리한다.
-
BeanDefinitionLoader 생성
-
AnnotatedBeanDefinitionReader 정의
-
XmlBeanDefinitionReader 정의
-
GroovyBeanDefinitionReader 정의
-
ClassPathBeanDefinitionScanner 정의 - ExcludeFilter 포함
-
-
위 단계에서 정의된 Reader/Scanner 에 BeanNameGenerator 를 통해 scan 된 각 Bean 의 이름을 구성하고 load
위 단계에서 지정된 BeanDefinitionLoader 는 Spring 구동을 위한 Annotation (or XML 등)의 Type 들을 Set 에 담고 있고 이를 통해 관련 Annotation 들을 Scan 하여 인스턴스(내부소스에서 BeanUtils.instantiateClass()를 사용한다.) 로 만든다. 이렇게 생성된 인스턴스정보들은 아래와 같다.
위의 정보처럼 includeFilters 를 통해 기본적으로 @Component 와 @ManagedBean 을 Scan 할 수 있게되고 @ManagedBean 은 JEE, EJB 의 자원 주입, 라이프사이클 콜백 및 인터셉터와 같은 small set 서비스를 지원하는 인터페이스이다.
이런 과정에 따라 SpringApplication 은 @Component 로 지정된 Class 를 찾게 되는데 잘 알고 있는 것처럼 @Component 로 지정되어 있는 @Configuration, @Service, @Controller, @Repository 로 지정된 Class 들을 찾아 관리 대상으로 등록한다.
Application 시작할 때의 로그
실행과정에 따른 결과로 Console 에서 해당 로그들을 확인할 수 있는데 대략 나누면 아래와 같다.
위의 이미지를 보면 알겠지만 PropertySource 에서 Spring Boot 에 미리 정의된 key 들을 찾는다. 이 key 들은 Spring 의 docs(https://docs.spring.io/spring-boot/docs/2.3.5.RELEASE/reference/html/appendix-application-properties.html) 를 통해 확인할 수 있고 관련 속성값들은 application.yml 또는 application.properties 를 통해 재정의할 수 있다.
spring 의 선언대로 Config 파일들을 location 별로 찾고 발견된 config 파일을 로드한다. 각 location 의 순서는 docs.spring.io/spring-boot/docs/2.3.5.RELEASE/reference/html/spring-boot-features.html#boot-features-external-config 의 선언에 따라 수행된다.
이처럼 log 를 잘 확인하면 SpringApplication 의 실행과정에 대한 상세정보를 확인할 수 있고 ConditionEvaluationReportLoggingListener 를 통해 실행과정에서의 정보(Positive matches, Negative matches, Exclusions, Unconditional classes) 를 report 받을 수 있다.