스프링 부트 내부 코드를 분석하는 도중 어떤 식으로 환경 설정, Environment가 설정되는지 궁금했습니다.
특히 지금 팀 프로젝트에서는 다음과 같이 profile group을 활용하고 있는데, 이게 어떤 과정으로 세팅되는지 궁금해 디버깅을 통해 분석해보고자 합니다.
application.yml의 내용은 다음과 같습니다.
spring:
profiles:
default: local
group:
local:
- console-logging
dev:
- console-logging
- file-warn-logging
- file-error-logging
- slack-error-logging
- file-info-request-logging
prod:
- file-warn-logging
- file-error-logging
- slack-error-logging
- file-info-request-logging
버전은 스프링 부트 3.0.8이며, Spring MVC를 사용했습니다.
스프링 부트에서 Environment는 다음과 같은 클래스를 거쳐 초기화됩니다.
SpringApplication 클래스는 자바 기반의 스프링 애플리케이션을 부트스트랩하는 데 사용되는, 애플리케이션 시작 시 첫 번째로 실행되는 진입점입니다.
스프링 부트 메인 메서드에서 SpringApplication.run()을 따라가다보면 new SpringApplication()으로 생성하는 코드를 확인할 수 있습니다.
Environment 설정 시 중요한 부분이 WebApplicationType 입니다.
WebApplicationType은 애플리케이션의 타입을 결정하는 enum으로, SERVLET/REACTIVE/NONE으로 구성되어 있습니다.
스프링 애플리케이션은 이 WebApplicationType에 따라 Environment를 포함한 다양한 설정에 활용됩니다.
WebFlux의 핵심인 DispatcherHnadler, MVC의 핵심인 DispatcherServlet, 기본적인 자바 서블릿과 스프링 웹 설정 여부를 통해 이 스프링 애플리케이션의 타입을 결정합니다.
MVC 환경이므로 WebApplicationType.SERVLET을 반환합니다.
이후 오버라이딩된 run()에서 ApplicationContext를 생성하고 refresh 를 하는 과정을 거쳐, 사용할 수 있는 SpringApplication을 반환하고 체이닝으로 start()를 호출하게 됩니다.
이 메서드에서 초기에 bootstrap, java.awt.headless, listener 설정 등을 진행하는데 여기서 주의깊게 봐야 하는 부분은 getRunListener() 입니다.
getSpringFactoriesInstances()를 통해 지정한 클래스인 SpringApplicationRunListeners에 listener를 세팅합니다.
위와 같이 SpringFactoriesLoader를 통해 META-INF/spring.factories에서 지정된 값을 조회합니다.
이는 대표적인 SPI(Service Provider Interface)입니다.
SpringApplicationRunListeners에 세팅될 클래스는 SpringApplicationRunListener 인터페이스의 구현체 EventPublishingRunListener 하나뿐입니다.
이후 listeners.starting()에 의해 초기화됩니다.
EventPublishingRunListener는 내부적으로 SpringApplication을 필드로 가지며, starting()에 의해 초기화된 여러 listeners를 가지고 있습니다.
이후 SpringApplicarion.run()에서 Environment 설정을 위해 prepareEnvironment() 메서드를 호출하는데, 이 때 파라미터로 넘겨집니다.
prepareEnvironment() 메서드에서는 getOrCreateEnvironment()를 호출합니다.
최초 실행이므로 environment는 존재하지 않기 때문에, applicationContextFactory.createEnvironment()를 호출합니다.
이 때, SpringApplication에서 필드에 정의된 DefaultApplicationContextFactory가 호출됩니다.
이 때 파라미터로 넘어온 WebApplicationType은 SERVLET이며, SPI를 통해 spring.factories에서 ContextFactory 정보를 조회합니다.
웹 환경의 Reactive와 Servlet에 해당하는 ContextFactory 정보를 조회하며, 이후 action.apply에 의해 다음과 같은 메서드가 수행됩니다.
결과적으로는 ApplicationServletEnvironment라는, 서블릿 환경에서 사용하는 Environment가 생성되어 반환됩니다.
이 때의 Environment는 default Profile이 default로 지정되어 있습니다.
이렇게 생성한 Environment는 파라미터로 전달된 listeners를 통해 필요한 설정을 세팅하게 됩니다.
SpringApplicationRunListener는 스프링 부트 애플리케이션의 생명 주기에 따른 이벤트를 감지하고 콜백 메서드를 수행하는 클래스입니다.
이를 관리하는 컬렉션은 SpringApplicationRunListeners이며, SpringApplication.run()에서 파라미터로 넘어온 상태입니다.
SpringApplicationRunListener.environmentPrepared()가 호출됩니다.
해당 메서드를 포함해, 스프링 부트 애플리케이션 생명 주기를 감지하고 ApplicationEvent를 발행하고 있음을 확인할 수 있습니다.
여기서 모든 메서드들이 multicastInitialEvent()를 호출하고 있음을 확인할 수 있습니다.
이는 EventPublishingRunListener가 필드로 가지고 있는 SimpleApplicationEventMulticaster에 의해 수행됩니다.
ApplicationEventMulticaster의 단순한 구현체로, ApplicationEvent를 모두에게 전파(= multicast)하는 역할을 수행합니다.
전파받은 listener가 필요 없는 ApplicationEvent를 알아서 무시합니다.
getApplicationListeners() 메서드를 통해 EventPublishingRunListener가 가지고 있던 listener 중 Environment 전처리 작업에 어울리는 listener인 ApplicationListener의 구현체 EnvironmentPostProcessorApplicationListener가 전달됩니다.
여기서 ApplicationListener.onApplicationEvent()를 통해 ApplicationEvent를 처리할 listener를 호출합니다.
ApplicationListener는 전파받은 ApplicationEvent를 처리하는 listener 입니다.
그 중 EnvironmentPostProcessorApplicationListener는 이름에서 알 수 있듯이 Environment에 대한 후처리를 담당하는 listener 입니다.
SimpleApplicationEventMulticaster에서 발행한 ApplicationEvent의 타입은 ApplicationEnvironmentPreparedEvent이므로 onApplicationEnvironmentPreparedEvent()가 호출됩니다.
getEnvironmentPostProcessors()에 의해 Environment를 설정할 수 있는 EnvironmentPostProcessor를 찾아서 postProcessEnvironment()를 수행합니다.
EnvironmentPostProcessor는 이름 그대로, Environment의 후처리기입니다.
Environment에 대한 후처리기는 모두 7개며, 지금 과정에서 살펴볼 것은 ConfigDataEnvironmentPostProcessor입니다.
이 후처리기는 properties/yaml/환경 변수 등의 설정을 관리할 수 있습니다.
진행하다 보면 processAndApply()를 수행하게 됩니다.
withProfiles()에서 스프링 부트 애플리케이션의 Profile을 설정합니다.
그래서 앞/뒤로 profiles 없이, profiles가 있는 상태로 추가적인 설정을 진행하는 것을 확인할 수 있습니다.
여기서 Profiles 객체를 생성하는 것을 확인할 수 있습니다.
Profiles는 스프링 애플리케이션의 Profile을 관리하는 역할을 수행합니다.
groups 필드는 spring.profiles.group의 설정을 세팅합니다.
필드를 보면 리스트 형식으로 관리하고 있음을 확인할 수 있습니다.
activeProfiles()는 현재 지정하지 않은 상태이므로 생략하겠습니다.
defaultProfiles는 spring.profiles.default의 값을 세팅합니다.
여기서 파라미터로 전달하는 Type은 Profiles의 inner enum이며, 현재 값은 DEFAULT인 spring.profiles.default 입니다.
여기서 binder.bind()를 통해 세팅을 진행합니다.
Binder는 환경 변수 값(Environment Properties)를 이름대로 바인딩하는 역할을 수행합니다.
오버라이딩된 bind() 메서드를 쭉 따라가다 보면 위와 같이 findProperty()로 설정 파일에 있는 속성(property)를 조회합니다.
이 때 Context.getSurces()를 통해 ConfigurationPropertySource에서 속성을 조회합니다.
ConfigurationPropertySource은 실제 환경 변수 값에 접근할 수 있는 인터페이스입니다.
실제 application.yml에 접근하는 ConfigurationPropertySource는 OriginTrackedMapPropertySource으로, 설정의 원점(설정이 정의되어 있는 위치)로 접근해서 특정 값(= spring.profiles.default)를 조회합니다.
확인하면 spring.profiles.default에 접근해서 그 값인 local을 반환하는 것을 확인할 수 있습니다.
이렇게 조회한 값은 쭉 반환됩니다.
이후 expandProfiles(getDefaultProfiles(environment, binder))에서 expandProfiles()가 실행되어 spring.profiles.default에 해당하는 그룹을 세팅합니다.
이를 통해 spring.profiles.default인 local과 group 관계인 console-logging 상태가 세팅되었습니다.
최종적으로 Servlet 환경의 Environment에 defaultProfiles가 세팅되었습니다.
이와 유사한 방식으로 다른 Environment property도 세팅됩니다.