스프링 부트를 사용해 자동 구성 정보의 일부 내용을 변경하거나 설정해야 할 때 Environment를 통해서 프로퍼티의 값을 가져와 활용할 수 있다.
실제 커스텀 빈을 등록해야 하는 과정은 연관되고 주입되어야 하는 빈들 간의 연관관계가 많은 경우 매우 복잡해지는데, 스프링 부트는 간단하게 자동 구성의 디폴트 설정을 변경하는 게 가능하다.
스프링 부트는 기본적으로 application.properties. xml, yml 등의 프로퍼티를 읽어오는 기능을 추가했다.
자동 구성에서 Environment 프로퍼티를 주입받아서 속성값을 읽고 싶을 때 스프링 부트의 모든 초기화 작업이 끝나고 나면 실행되는 코드를 만드는 방법 중에 ApplicationRunner 인터페이스를 구현한 오브젝트 또는 람다식을 빈으로 등록하는 방법이 있다.
@FunctionalInterface
public interface ApplicationRunner {
/**
* Callback used to run the bean.
* @param args incoming application arguments
* @throws Exception on error
*/
void run(ApplicationArguments args) throws Exception;
}
이는 초기화 작업 및 컨테이너의 검사 등 전체 로딩 이후 초기화를 진행할 때 자주 사용되는 방법이다.
@Bean
ApplicationRunner applicationRunner(Environment env) {
return args -> {
String name = env.getProperty("my.name");
System.out.println(name);
};
}
이제부터 프로퍼티를 다양한 곳에서 읽어오는 시도를 해보자.
첫 번째는 application.properties 파일에서 원하는 값을 읽어오는 것이다.
my.name=Oncerun
public static void main(String[] args) {
MySpringBoot.run(MySpringbootApplication.class, args);
}
public class MySpringBoot {
public static void run(Class<?> applicationClass, String[] args){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(){
@Override
protected void onRefresh() {
super.onRefresh();
ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
DispatcherServlet servlet = this.getBean(DispatcherServlet.class);
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet", servlet).addMapping("/*");
});
webServer.start();
}
};
applicationContext.register(applicationClass);
applicationContext.refresh();
ApplicationRunner initRunner = applicationContext.getBean(ApplicationRunner.class);
try {
initRunner.run(new DefaultApplicationArguments(args));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
이는 스프링 부트의 서블릿 컨테이너를 커스텀한 클래스로 시작하도록 만드는 코드이며 Runner를 시작하는 코드가 아마 저렇게 되어있을 것 같다.
아마 다른 분들은 SpringApplication 클래스의 static run 메서드를 사용하고 있을 것이다.
public static void main(String[] args) {
SpringApplication.run(MySpringbootApplication.class, args);
}
실제 SpringApplication 클래스의 Run 메서드를 디버깅해 보면 다음과 같은 메서드가 나온다.
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
실제 시작 시간을 측정하는 로직도 있고 콘텍스트를 시작하기 위해 부트 스트랩 컨텍스를 만드는 코드가 있다.
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
코드 중 callRunners()라는 메서드가 존재하는데 이 내부에는 ApplicatiopnRunner.class 타입의 빈을 List에 추가하는 로직이 있고 이를 실행하는 로직이 존재한다.
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
try {
(runner).run(args);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
}
}
이를 보면 ApplicationRunner를 구현한 빈이 여러 개가 존재할 수 있나 보다.
굳이 하나의 ApplicationRunner에서 모든 초기화 코드를 사용하지 않고 분리하여 각각의 Runner를 구성할 수 있는 것으로 보이며
AnnotationAwareOrderComparator.sort(runners);
해당 코드를 보면 @Order 어노테이션을 통해 sorting이 가능한 것으로 보이며, 초기화 작업에 대해 실행 순서를 지정할 수 있는 것으로 보인다.
@Bean
@Order(1)
ApplicationRunner applicationRunner(Environment environment) {
return args -> {
String name = environment.getProperty("my.name");
System.out.println("order 1");
System.out.println("my name" + name);
};
}
@Bean
@Order(2)
ApplicationRunner applicationRunner2(Environment environment) {
return args -> {
String name = environment.getProperty("my.name");
System.out.println("order 2");
System.out.println("my name" + name);
};
}
다음과 같이 설정하고 실행하면 다음과 같은 로그를 확인할 수 있다.
여기까지 정리하면 다음과 같다.
application을 prefix로 갖는 xml, yml, properties 파일을 통해 환경변수들을 스프링 부트가 주입해 주는 Environment 빈을 통하여 쉽게 접근하여 값을 가져올 수 있다.
만약 스프링 컨테이너의 로딩이 끝난 이후 초기화가 필요한 작업이 있다면 ApplicationRunner Type의 빈들을 @Order을 통해 순서를 지정하여 초기화 작업을 진행할 수 있다.
두 번째는 시스템 환경변수이며, 이는 첫 번째 방법보다 우선순위가 높다.
@Bean
@Order(1)
ApplicationRunner applicationRunner(Environment environment) {
return args -> {
String name = environment.getProperty("my_name");
System.out.println("order 1");
System.out.println("my name" + name);
};
}
@Bean
@Order(2)
ApplicationRunner applicationRunner2(Environment environment) {
return args -> {
String name = environment.getProperty("my.name");
System.out.println("order 2");
System.out.println("my name" + name);
};
}
실제 언더바를 사용한 경우나 DOT (.)을 사용한 경우 전부 시스템 환경변수의 값이 사용됐다.
이걸로 보아 환경변수에서는 dot을 사용하지 못하기 때문에 dot을 언더 스코어로 치환해 준다는 것을 확인할 수 있다.
세 번째는 시스템 프로퍼티로 이는 더 높은 우선순위를 가지고 있다.
이는 보통 -D 옵션을 통해 프로퍼티를 사용하는 값을 말한다.
결과는 다른 프로퍼티 설정 파일 및 환경 변수보다 우선순위가 높아 해당 값을 가져온다.
다음 관심사는 다음과 같은데, 우리가 등록한 빈에 대한 세부 설정을 어떻게 환경 변수로 통제하는 지에 대한 의문이다.
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
@Bean(name = "tomcatWebServerFactory")
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
다음과 같은 서블릿 컨테이너 구현체를 스프링 부트가 래핑 한 TomcatServletWebServerFactory가 있다.
public ServletWebServerFactory servletWebServerFactory() {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
tomcatServletWebServerFactory.setPort(9090);
return tomcatServletWebServerFactory;
}
어떻게 하면 다음 코드와 같이 설정파일을 통해 서블릿 컨테이너의 listen port를 9090으로 변경할 수 있을까?
@Bean(name = "tomcatWebServerFactory")
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory(Environment env) {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
tomcatServletWebServerFactory.setPort(Integer.parseInt(Objects.requireNonNull(env.getProperty("port"))));
return tomcatServletWebServerFactory;
}
이렇게 하지 않을까? 우리는 Bean을 등록할 때 Envionment를 주입받을 수 있다.
그 이유는 environment 또한 context의 또 다른 기능 중 하나이고 이 자체는 Spring Context를 래핑 한 spring context이기 때문이다.
public interface ApplicationContext
extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver{}
이는 ApplicationContext의 인터페이스를 보면 쉽게 알 수 있다.
이렇듯 우리는 설정파일의 프로퍼티 값을 통하여 빈의 설정정보를 조작하는 방법을 알았다.
하지만 실제 빈들의 설정정보는 너무나 많고 이를 이렇게 비효율적으로 처리하지는 않는다.
다음에는 이를 스프링 부트가 어떻게 처리했는지 알아볼 것이다.
'Spring|Spring-boot' 카테고리의 다른 글
Spring Boot 좀 더 자세히. (2) | 2023.02.04 |
---|---|
Properties Bean PostProcessor (0) | 2023.02.02 |
Spring boot AutoConfiguration and Conditional (0) | 2023.01.26 |
Spring Boot @AutoConfiguration (0) | 2023.01.24 |
Standalone Application (0) | 2023.01.22 |
댓글