스프링부트

외부 설정 파일 로딩하기

알쓸개잡 2024. 8. 18.

이번 포스팅에서는 spring boot 애플리케이션에서 application.yml (혹은 application.properties) 파일 외에 애플리케이션 외부의 설정 파일을 로딩하는 방법에 대해서 정리하고자 한다.

  • Spring Boot 이벤트 리스너를 확장 구현하는 방법
  • spring.config.import를 이용하는 방법
  • @PropertySource로 외부 설정항목을 주입하는 방법

 

Spring Boot profile 적용 메커니즘

  • Spring Boot 애플리케이션의 모든 설정 (시스템환경설정, JVM 환경설정, 애플리케이션 설정등)은 Environment의 PropertySource 리스트에 저장하여 처리한다.
  • application.yml 파일에 여러 profile 설정이 지정되어 있는 경우 파일의 아래에서 위 순서로 지정된 profile 별 PropertySource가 저장된다.

예를 들어 다음과 같은 설정 파일이 있을 때

spring:
  application:
    name: external-config

myconfig:
  name: default-config
  key1: key1-default-config

---

spring:
  config:
    activate:
      on-profile: usekey

myconfig:
  key1: key1-usekey-config

---

spring:
  config:
    activate:
      on-profile: prod

myconfig:
  name: prod-config

---

spring:
  config:
    activate:
      on-profile: dev

myconfig:
  name: dev-config
  key1: key1-dev-config

위 설정은 dev, prod, usekey, default (profile이 지정되지 않은 영역) 순서로 로딩되며 active profile로 지정된 profile + default profile에 대해서만 Environment의 PropertySource 리스트에 저장된다.

active profile이 dev, usekey라고 했을 때 위 설정에서 저장되는 PropertySource는 dev, usekey, default 순서로 Environment의 PropertySource 리스트에 저장된다.

설정 property를 찾는 메커니즘은 Environment의 PropertySource 리스트에서 순서대로 설정 property를 찾는다.

즉, myconfig.key1 프로퍼티를 찾을 때 dev profile에서 우선 찾고 해당 property 항목이 없으면 usekey profile에서 찾고 여기에도 없으면 default profile에서 찾게 된다. 이 케이스에서 myconfig.key1 property의 값은 key1-dev-config가 된다.

 

아래 설정과 같이 usekey profile 설정이 dev profile 설정보다 아래 위치에 정의되어 있다면 Environment의 PropertySource 리스트에 저장되는 순서는 usekey, dev, default 순서가 되므로 active profile이 dev, usekey라고 했을 때 myconfig.key1 property의 값은 key1-usekey-config 가 된다.

spring:
  application:
    name: external-config

myconfig:
  name: default-config
  key1: key1-default-config

---

spring:
  config:
    activate:
      on-profile: prod

myconfig:
  name: prod-config

---

spring:
  config:
    activate:
      on-profile: dev

myconfig:
  name: dev-config
  key1: key1-dev-config

---

spring:
  config:
    activate:
      on-profile: usekey

myconfig:
  key1: key1-usekey-config

 

즉, 설정 파일에 여러 프로파일에 대한 설정이 정의되어 있다면 active 프로파일 설정의 순서와 상관없이 설정 파일 아래에서 위 순서로 프로파일 설정이 우선 적용된다. (이 경우 active 프로파일의 설정 순서는 상관없음)

spring boot 설정 로딩
spring boot 설정 로딩 1

 

 

프로파일 별로 설정 파일이 있는 경우 (ex) application-dev.yml, application-usekey.yml) 에는 active 프로파일이 지정된 순서에서 뒤에 지정된 프로파일 설정이 우선 적용된다. (이 경우 active 프로파일 설정 순서가 상관함)

예를 들어 아래 세 개의 설정 파일이 있다고 했을 때

# application-usekey.yml
myconfig:
  key1: key1-usekey-config
# application-dev.yml
myconfig:
  name: dev-config
  key1: key1-dev-config
# application.yml
spring:
  application:
    name: external-config

myconfig:
  name: default-config
  key1: key1-default-config

active 프로파일이 dev, usekey 순서로 지정되어 있고 myconfig.key1 설정을 찾을 때 뒤에 active profile에서 뒤에 지정된 usekey 프로파일이 우선적으로 적용되어 myconfig.key1은 key1-usekey-config 가 된다. myconfig.name property는 usekey 프로파일 파일에 없기 때문에 dev 프로파일 설정에서 myconfig.name property를 찾아서 존재하면 해당 property가 사용되어 myconfig.name property 값은 dev-config가 된다. 만약 active 프로파일 지정 순서가 usekey, dev 라면 dev 프로파일이 우선적으로 적용되어 myconfig.key1은 key1-dev-config 가 된다.

spring boot 프로파일별 파일 로딩 순서
spring boot 프로파일별 파일 로딩 순서

Spring Boot 이벤트 리스너를 확장 구현하는 방법

우선 아래 포스팅을 참고하면 도움이 될 만한 내용이 있을 것 같다.

2023.10.02 - [스프링부트] - application events and listeners

 

Spring Boot 애플리케이션을 기동 하는 과정에 여러 단계들을 거쳐서 애플리케이션을 구동하게 되는데 그중 하나의 과정은 여러 설정들을 Environment 인스턴스에 로딩하는 것이다.

이와 관련하여 애플리케이션 초기화 과정에서 실행되는 두 가지 중요한 과정이 있는데 다음과 같다.

  • EnvironmentPostProcessor
    • EnvironmentPostProcessor는 ApplicationEnvironmentPreparedEvent가 발생하기 전에 실행된다.
    • Spring Boot는 애플리케이션 시작 초기 단계에서 EnvironmentPostProcessor를 사용하여 Envirionment를 초기화하고 설정 파일을 로드한다.
  • ApplicationEnvironmentPreparedEvent 발생
    • EnvironmentPostProcessor 실행이 완료된 후 Spring Boot는 Environment가 준비된 것을 감지하고 ApplicationEnvironmentPreparedEvent를 발생시킨다.

위 두 실행은 모두 ApplicationContext가 생성되기 전에 발생하므로 EnviroinmentPostProcessor 단계든 ApplicationEnvironmentPreparedEvent 단계든 어디에서 외부 설정 파일을 로딩하도록 해도 상관없다. 두 단계 모두 Environment 인스턴스를 전달받을 수 있으며 외부 설정 파일을 로딩하여 Environment 인스턴스에 추가해 주면 된다.

 

Spring Boot 애플리케이션 초기화 과정의 이벤트 혹은 리스너를 확장한 클래스를 작성하여 동작시키기 위해서는 resources/META-INF/spring.factories 에 해당 클래스를 등록해줘야 한다.

 

EnvironmentPostProcessor에서 처리하는 방식

package org.example.external.config.listener;
...

public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor {
    @Override
    public void postProcessEnvironment( ConfigurableEnvironment environment, SpringApplication application ) {
        String rootDirectory = System.getProperty( "user.dir" );
        YamlPropertySourceLoader yamlPropertySourceLoader = new YamlPropertySourceLoader();
        FileSystemResource fileSystemResource = new FileSystemResource( rootDirectory + "/external-config-all.yml" );

        try {
            List<PropertySource<?>> propertySourceList;
            List<String> activeProfiles = Arrays.asList( environment.getActiveProfiles() );
            //propertySourceList 에는 프로파일별 PropertySource가 저장된다.
            propertySourceList = yamlPropertySourceLoader.load( "external-config", fileSystemResource );
            PropertySource<?> defaultPropertySource = propertySourceList.get( 0 );
            //아래 -> 위 순서로 프로파일을 적용하기 위함.
            Collections.reverse( propertySourceList );

            propertySourceList.forEach( propertySource -> {
                String profile = (String) propertySource.getProperty( "spring.config.activate.on-profile" );

                if ( activeProfiles.contains( profile ) ) {
                    environment.getPropertySources().addLast( propertySource );
                }
            } );

            //가장 마지막에 external-config-all.yml 파일의 default 프로파일에 대한 PropertySource를 추가한다.
            environment.getPropertySources().addLast( defaultPropertySource );
        }
        catch ( IOException e ) {
            System.out.println("fail to load external config, " + fileSystemResource.getFile().getAbsolutePath() + ", " + e.getMessage());
        }
    }
}

위 코드는 EnvironmentPostProcessor의 postProcessEnvironment 메서드를 오버라이드 하여 초기화 과정에서 실행된다.

위 코드를 실행시키기 위해서는 resources/META-INF/spring.factories 에 아래와 같이 등록을 해야 한다.

org.springframework.boot.env.EnvironmentPostProcessor=\
  org.example.external.config.listener.CustomEnvironmentPostProcessor

직접 작성한 클래스 파일의 패키지 경로는 본인의 코드에 맞게 수정이 필요하다.  spring boot 3.3.2에서 EnvironmentPostProcessor 클래스의 패키지 경로는 org.springframework.boot.env이지만 다른 spring boot 버전의 경우 패키지 경로가 다를 수 있으므로 패키지 경로를 잘 확인하자.

 

 

위 코드는 프로젝트의 루트 디렉터리의 external-config-all.yml 설정 파일을 YamlPropertySourceLoader 클래스를 이용하여 로딩 후 여러 PropertySource를 얻어 Environment 인스턴스에 추가시키는 코드다.

YamlPropertySourceLoader.load() 메서드를 통해서 외부 설정 파일에 대한 PropertySource 리스트를 얻게 되는데 PropertySource 리스트에는 위에서 아래로 지정된 profile 설정 순으로 들어간다.

 

PropertySource<?> defaultPropertySource = propertySourceList.get( 0 );
Collections.reverse( propertySourceList );

위 코드는 default profile에 대한 PropertySource를 미리 얻어 두고 PropertySource 리스트를 역순으로 변경을 한 코드다.

이유는 Spring Boot profile 적용 메커니즘을 따르기 위해서다.

(yml 파일의 물리적인 위치 아래 -> 위 순으로 profile을 적용시키기 위해

- YamlPropertySourceLoader는 위 -> 아래 순으로 로딩하므로)

물론 직접 로딩한 외부 설정 파일에 대해서 Spring boot profile 적용 메커니즘에 맞추지 않고 다른 방식으로 커스텀하게 적용시킬 수도 있다. (커스텀하게 직접 구현하는 이점은 여기에 있겠다..)

propertySourceList.forEach( propertySource -> {
    String profile = (String) propertySource.getProperty( "spring.config.activate.on-profile" );

    if ( activeProfiles.contains( profile ) ) {
        environment.getPropertySources().addLast( propertySource );
    }
} );

//가장 마지막에 external-config-all.yml 파일의 default 프로파일에 대한 PropertySource를 추가한다.
environment.getPropertySources().addLast( defaultPropertySource );

위 코드는 외부 설정 파일에서 읽어온 프로파일별 PropertySource에서 지정된 프로파일에 대해서만 Environment의 PropertySource 리스트에 추가하는 코드다. 당연한 얘기겠지만 지정된 프로파일을 적용하려면 지정된 프로파일의 PropertySource만 직접 넣어줘야 한다.

마지막으로 default 프로파일에 대한 PropertySource를 마지막에 추가해 줘야 한다.

 

ApplicationListener에서 처리하는 방식

내부 로직은 CustomEnvironmentPostProcessor 처리 로직과 동일하고 인터페이스의 변경 차이 밖에 없다. ApplicationEnvironmentPreparedEvent 인스턴스를 통해서 Environment 인스턴스를 얻어서 처리하면 된다.

public class ApplicationEnvironmentPreparedEventListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
    @Override
    public void onApplicationEvent( ApplicationEnvironmentPreparedEvent event ) {
        String rootDirectory = System.getProperty( "user.dir" );
        YamlPropertySourceLoader yamlPropertySourceLoader = new YamlPropertySourceLoader();
        FileSystemResource fileSystemResource = new FileSystemResource( rootDirectory + "/external-config-all.yml" );

        try {
            List<PropertySource<?>> propertySourceList;
            ConfigurableEnvironment environment = event.getEnvironment();
            List<String> activeProfiles = Arrays.asList( environment.getActiveProfiles() );
            propertySourceList = yamlPropertySourceLoader.load( "external-config", fileSystemResource );
            PropertySource<?> defaultPropertySource = propertySourceList.get( 0 );
            Collections.reverse( propertySourceList );

            propertySourceList.forEach( propertySource -> {
                String profile = (String) propertySource.getProperty( "spring.config.activate.on-profile" );

                if ( activeProfiles.contains( profile ) ) {
                    environment.getPropertySources().addLast( propertySource );
                }
            } );

            environment.getPropertySources().addLast( defaultPropertySource );
        }
        catch ( IOException e ) {
            System.out.println("fail to load external config, " + fileSystemResource.getFile().getAbsolutePath() + ", " + e.getMessage());
        }
    }
}

위 코드를 동작시키기 위해서는 resources/META-INF/spring.factories 파일에 다음과 같이 클래스를 등록한다.

org.springframework.context.ApplicationListener=\
  org.example.external.config.listener.ApplicationEnvironmentPreparedEventListener

내부 동작은 CustomEnvironmentPostProcessor과 동일하다.

 

애플리케이션 초기화 과정에서 이벤트 리스너 혹은 프로세서를 확장 구현하는 방식은 애플리케이션 설정 파일(application.yml)에 대한 로딩이 완료된 후 이루어지는데 Environment의 PropertySource 리스트에서 application.yml 보다 우선적으로 적용시킬 수도 있고(addFirst) 뒤로(addLast) 적용시킬 수도 있다. 
만약 external-config-{profile}. yml 파일에 대해서 active 프로파일에 맞게 프로파일별 파일을 로딩하려면 이에 대한 구현을 직접 해줘야 한다.

 

spring.config.import를 이용하는 방법

spring boot는 외부 설정 파일을 spring.config.import 설정을 통해서 로딩할 수 있다.

# application.yml

spring:
  application:
    name: external-config
  config:
    import: optional:file:${user.dir}/external-config.yml

위와 같이 지정을 하게 되면 애플리케이션 초기화 단계에서 <project-root-directory>/external-config.yml 파일을 로딩한다.

만약 active 프로파일이 지정되어 있고 external-config-{profile}.yml 파일이 있다면 알아서 로딩해 준다.

 

로딩할 파일이 여러 개인 경우 아래와 같이 설정한다.

# application.yml

spring:
  application:
    name: external-config
  config:
    import: 
      - optional:file:${user.dir}/external-config.yml
      - optional:file:${user.dir}/external-config2.yml

 

optional: 은 파일 존재 여부에 따른 동작을 정의하는데 optional: 이 붙은 경우 지정된 경로에 설정 파일이 존재하지 않더라도 예외를 발생시키지 않도록 한다. 파일이 존재하면 해당 파일의 설정이 로드되고, 파일이 없으면 해당 파일은 무시된다.

 

file: 은 참조하는 설정 파일이 파일시스템 상의 경로에 있는 파일을 의미한다.

classpath: 를 지정하면 클래스패스 경로에 있는 파일을 의미한다.

 

이벤트 리스너 혹은 프로세서를 통해서 외부 설정 파일을 직접 로딩하는 방식과 달리 spring.config.import로 지정된 파일은 애플리케이션 초기화 과정에서 application.yml 보다 우선적으로 로딩된다.
예를 들어 외부 설정 파일과 application.yml 설정 모두 external.key1 설정이 있다면 외부 설정파일의 external.key1 값이 적용된다는 의미다.

 

동작확인

external-config.yml

external:
  key1: defaultValue1
  key2: defaultValue2

 

external-config-dev.yml

external:
  key1: devValue1
  key2: devValue2

 

external-config-prod.yml

external:
  key1: prodValue1
  key2: prodValue2

 

application.yml

spring:
  application:
    name: external-config
  config:
    import:
      - optional:file:${user.dir}/external-config.yml

 

external property mapping class

@ConfigurationProperties( prefix = "external" )
@Getter
@Setter
public class ExternalProperties {
    private String key1;
    private String key2;
}

 

application main class

@SpringBootApplication
@ConfigurationPropertiesScan
@RequiredArgsConstructor
public class ExternalConfigApplication implements CommandLineRunner {
    private final ExternalProperties externalProperties;

    public static void main( String[] args ) {
        SpringApplication.run( ExternalConfigApplication.class, args );
    }

    @Override
    public void run( String... args ) {
        System.out.println( "external key1: " + externalProperties.getKey1() );
        System.out.println( "external key2: " + externalProperties.getKey2() );
    }
}

 

active 프로파일을 dev, prod로 지정했을 때 결과는 다음과 같다.

external key1: prodValue1
external key2: prodValue2

 

active 프로파일을 prod, dev로 지정했을 때 결과는 다음과 같다.

external key1: devValue1
external key2: devValue2

 

@PropertySource로 외부 설정을 로드하여 property를 주입하는 방법

@Configuration 클래스에 @PropertySource를 이용하여 지정된 파일에서 설정 프로퍼티를 주입할 수 있다.

아쉽게도 @PropertySource는 yml 은 지원하지 않는다.

 

# external-config.properties

external.key1=defaultValue1
external.key2=defaultValue2

위와 같은 properties 파일이 있을 때 @PropertySource 애노테이션으로 설정 프로퍼티를 주입받는 방법은 다음과 같다.

@Getter
@Configuration
@PropertySource( "file:${user.dir}/external-config.properties" )
public class ExternalConfigByPropertySource {

    @Value( "${external.key1}" )
    private String key1;

    @Value( "${external.key2}" )
    private String key2;
}

위 코드는 <프로젝트 root directory>/external-config.properties 파일을 로드하여 key1, key2 변수에 각각 external.key1, external.key2 프로퍼티를 주입한다.

 

@SpringBootApplication
@RequiredArgsConstructor
public class ExternalConfigApplication implements CommandLineRunner {
    private final ExternalConfigByPropertySource externalConfigByPropertySource;

    public static void main( String[] args ) {
        SpringApplication.run( ExternalConfigApplication.class, args );
    }

    @Override
    public void run( String... args ) {
        System.out.println( "key1: " + externalConfigByPropertySource.getKey1());
        System.out.println( "key2: " + externalConfigByPropertySource.getKey2());
    }
}

 

위 코드의 결과는 다음과 같다.

key1: defaultValue1
key2: defaultValue2

 

@PropertySource를 통해서 로딩된 설정은 애플리케이션 초기화 과정 이후에 로딩되는 설정으로 가장 우선순위가 낮다고 볼 수 있다. 
예를 들어 external-config.properties 파일과 application.yml 설정 모두 external.key1 설정이 있다면 application.yml의  external.key1 값이 적용된다는 의미다.

 

지금까지 spring boot 의 설정 로딩 순서와 외부 설정을 로딩하여 사용하는 몇 가지 방법을 정리해 보았다.

끝.

댓글

💲 추천 글