profile image

L o a d i n g . . .

728x90

해당 포스팅은 [Spring Boot] 대체 어떻게 실행되는 걸까? 포스팅의 일부입니다.

2022.05.01 - [Java] - [Spring Boot] 대체 어떻게 실행되는 걸까?

 

[Spring Boot] 대체 어떻게 실행되는걸까 ?

beanFactory.registerSingleton("springApplicationArguments", applicationArguments); Spring boot를 사용하다 보면 이 마법 같은 프레임워크가 어떻게 동작하는지 궁금할 때가 있습니다. Spring boot는 어떻게..

code-run.tistory.com

 

Spring boot의 환경변수를 설정하는 방법은 다양합니다. application.properties 설정, system의 환경변수 또는 command line을 통해 전달한 환경변수를 사용하는 등 다양한 방법이 존재합니다. 또한 동일한 환경변수에 대해서는 우선순위가 존재합니다. 이번 포스팅에서는 spring boot가 어떻게 환경변수를 설정하는지에 대해 알아보겠습니다.

 

환경변수는 어떻게 설정되는 걸까 

Spring boot는 main()의 SpringApplication.run(...)을 시작으로 spring boot application 실행에 필요한 작업을 수행합니다. 해당 메서드를 따라가다 보면 Spring boot와 관련된 설정을 수행하는 public ConfigurableApplicationContext run(String... args) 메서드를 확인할 수 있습니다. 

public ConfigurableApplicationContext run(String... args) {
    long startTime = System.nanoTime();
    DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
    ConfigurableApplicationContext context = null;
    this.configureHeadlessProperty();
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);

    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        this.configureIgnoreBeanInfo(environment);
        Banner printedBanner = this.printBanner(environment);
        context = this.createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        this.refreshContext(context);
        this.afterRefresh(context, applicationArguments);
        Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
        }

        listeners.started(context, timeTakenToStartup);
        this.callRunners(context, applicationArguments);
    } catch (Throwable var12) {
        this.handleRunFailure(context, var12, listeners);
        throw new IllegalStateException(var12);
    }

    try {
        Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
        listeners.ready(context, timeTakenToReady);
        return context;
    } catch (Throwable var11) {
        this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
        throw new IllegalStateException(var11);
    }
}

해당 메서드 내의 ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments) 코드를 통해 스프링이 환경변수를 읽어 들일 propertySource를 설정합니다. 그럼 해당 코드를 하나하나 살펴보겠습니다. 

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    ConfigurableEnvironment environment = this.getOrCreateEnvironment(); // 1.1
    this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs()); // 1.2
    ConfigurationPropertySources.attach((Environment)environment); // 1.3
    listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment); // 1.4
    DefaultPropertiesPropertySource.moveToEnd((ConfigurableEnvironment)environment); // 1.5
    Assert.state(!((ConfigurableEnvironment)environment).containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties."); // 1.6
    this.bindToSpringApplication((ConfigurableEnvironment)environment); // 1.7
    if (!this.isCustomEnvironment) { // 1.8
        environment = this.convertEnvironment((ConfigurableEnvironment)environment);
    }

    ConfigurationPropertySources.attach((Environment)environment); // 1.9
    return (ConfigurableEnvironment)environment;
}

prepareEnvironment 메서드는 ConfigurableEnvironment를 생성해서 반환하는데 ConfigurableEnvironment가 무엇인지부터 확인해보겠습니다. Spring docs의 정의를 보면 

Provides facilities for setting active and default profiles and manipulating underlying property sources. Allows clients to set and validate required properties, customize the conversion service and more through the ConfigurablePropertyResolver superinterface. 

해석해보면 ConfigurableEnvironment를 통해서 active 그리고 default profile을 설정할 수 있고 spring에 기본적으로 설정된 property source에 대한 설정을 조작할 수 있습니다. 또한 사용자들은  super interface인 ConfigurablePropertyResolver을 통해서 properties를 검증하거나 conversion service를 커스터마이징이 가능합니다.  그럼 각각의 코드가 어떤 역할을 하는지 파악함으로써 ConfigurableEnvironment 어떻게 동작하는지 알아보겠습니다. 

 

ConfigurableEnvironment environment = this.getOrCreateEnvironment() 

private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    } else {
        switch(this.webApplicationType) {
        case SERVLET:
            return new ApplicationServletEnvironment();
        case REACTIVE:
            return new ApplicationReactiveWebEnvironment();
        default:
            return new ApplicationEnvironment();
        }
    }
}

getOrdefaultEnvironment() 메서드는 webApplicationType에 따라서 적합한 ConfigurableEnvironment 구현체를 제공합니다. 참고로 webApplicationType(enum)은 다음 코드에 의해 설정됩니다. 

static WebApplicationType deduceFromClasspath() {
    if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", 
    	(ClassLoader)null) && 
        !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", 
        (ClassLoader)null) && 
        !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", 
        (ClassLoader)null)) {
        return REACTIVE;
    } else {
        String[] var0 = SERVLET_INDICATOR_CLASSES;
        int var1 = var0.length;

        for(int var2 = 0; var2 < var1; ++var2) {
            String className = var0[var2];
            if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
                return NONE;
            }
        }

        return SERVLET;
    }
}

SERVLET, REACTIVE 그리고 그 이외의 webApplicationType은 모두 공통적으로 StandardEnvironment 클래스를 구현합니다. 

public class StandardEnvironment extends AbstractEnvironment {
    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

    public StandardEnvironment() {
    }

    protected StandardEnvironment(MutablePropertySources propertySources) {
        super(propertySources);
    }

    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new PropertiesPropertySource("systemProperties", this.getSystemProperties()));
        propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", this.getSystemEnvironment()));
    }
}

참고로 system property는 java를 실행할 때 설정할 수 있습니다(예를 들면 command line으로 실행 시

-Dpropertyname=value). 그와 별개로 system environment는 java가 실행되는 운영체제의 환경변수(export 커맨드로 설정되는)를 의미합니다. 

다음으로는 StandardEnvironment가 상속하는 AbstractEnvironment에 대해 알아보겠습니다.

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";
    public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";
    public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";
    protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";
    protected final Log logger;
    private final Set<String> activeProfiles;
    private final Set<String> defaultProfiles;
    private final MutablePropertySources propertySources;
    private final ConfigurablePropertyResolver propertyResolver;
    
    ... 
    
}

AbstractEnvironment는 profile을 설정하는 메서드 위주로 구성되어있습니다. 

 

this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs()); 

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
    if (this.addConversionService) {
        environment.setConversionService(new ApplicationConversionService());
    }

    this.configurePropertySources(environment, args);
    this.configureProfiles(environment, args);
}

this.addConversionService는 spring boot가 실행될 때 자동으로 true로 설정된다.

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.sources = new LinkedHashSet();
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    this.addCommandLineProperties = true;
    this.addConversionService = true; // 자동으로 true 설정
    
    ...
}

ConversionService 인터페이스는 convert(Object, Class) 메서드를 통해 thread-safe한 형태로 type conversion을 수행할 수 있도록 합니다. 디버깅 모드를 통해 ConversionService의 converter에 대해 살펴보겠습니다. 

converters 예시

this.configurePropertySources(environment, args)는 PropertySource(name/value 쌍의 값을 저장하는 source를 나타내는 추상 클래스)를 추가, 삭제 또는 재배치를 수행하는 메서드입니다. 

 protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
    MutablePropertySources sources = environment.getPropertySources();
    if (!CollectionUtils.isEmpty(this.defaultProperties)) {
        DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources);
    }

    if (this.addCommandLineProperties && args.length > 0) {
        String name = "commandLineArgs";
        if (sources.contains(name)) {
            PropertySource<?> source = sources.get(name);
            CompositePropertySource composite = new CompositePropertySource(name);
            composite.addPropertySource(new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
            composite.addPropertySource(source);
            sources.replace(name, composite);
        } else {
            sources.addFirst(new SimpleCommandLinePropertySource(args));
        }
    }

}

this.configureProfiles(environment, args) 메서드는 특이하게도 아무런 로직이 존재하지 않습니다. 혹시 이유를 아시는 분이 계신다면 공유 부탁드리겠습니다. 

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {}

 

ConfigurationPropertySources.attach((Environment) environment);

environment의 PropertySource에 ConfigurationPropertySource를 연동함으로써 PropertySourcesPropertyResolver의 메서드를 사용할 수 있도록 합니다. PropertySourcesPropertyResolver은 다음과 같은 기능을 수행합니다. 

The attached resolver will dynamically track any additions or removals from the underlying Environment property sources.

 

listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);

Environment는 준비됐지만 ApplicationContext는 아직 준비되지 않은 단계에서 실행되는 메서드입니다. 만약 ApplicationListener<ApplicationEnvironmentPreparedEvent>listener로 등록하게 되면 해당 메서드가 실행되면서 등록된 listener을 실행시킵니다. 

@SpringBootApplication
public class SpringBootAutowireApplication {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(SpringBootAutowireApplication.class);
        springApplication.addListeners(new CustomListener());
        springApplication.run(args);

    }
}

class CustomListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        System.out.println("Called when listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment; is called");
    }
}​

listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment) environment); 호출 결과

 

DefaultPropertiesPropertySource.moveToEnd((ConfigurableEnvironment)environment);​

인자로 주어진 ConfigurableEnvironment에서 defaultProperties를 가장 마지막 source로 이동시킵니다. 

public static void moveToEnd(MutablePropertySources propertySources) {
    PropertySource<?> propertySource = propertySources.remove("defaultProperties");
    if (propertySource != null) {
        propertySources.addLast(propertySource);
    }
}

remove("defaultProperties") 메서드는 defaultProperties 이름으로 등록된 PropertySource를 제거하고, 제거된 PropertySource를 리턴합니다만약 defaultProperties라는 이름을 가진 PropertySource가 제거됐다면 (제거 후 리턴됐다면) propertySources의 가장 마지막 위치에 이동시킵니다. 

 

Assert.state(((ConfigurableEnvironment)environment).containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");

Property를 통해서 spring.main.environment-prefix가 설정됐는지 확인합니다. 만약 설정이 됐다면 다음과 같은 Exception을 발생시킵니다.

spring.main.environment-prefix 가 properties 파일에 설정된 경우 발생하는 exception

 

this.bindToSpringApplication((ConfigurableEnvironment)environment);

SpringApplication에 environment를 결합시킵니다. 

 

if (!this.isCustomEnvironment) ...

별다른 설정이 없다면 this.isCustomEnvironment는 false값으로 설정됩니다. 따라서 environment = this.convertEnvironment((ConfigurableEnvironment)environment); 는 실행되지만 spring 2.7.0 이후로는 Deprecated 예정인 메서드입니다. 

 

ConfigurationPropertySources.attach((Environment)environment);

(3)과 동일

 

결론

요약하자면 spring boot는 SpringApplication의 run 메서드 내부의 this.prepareEnvironment(listeners, bootstrapContext, applicationArguments); 를 통해서 spring boot에서 사용할 environment를 생성하고 설정합니다. 내부적으로 property source를 찾고 등록하며 향후 spring boot application에서 사용할 property를 설정하는데 초석이 됩니다. 

 

728x90
복사했습니다!