Spring boot에서 Custom yaml(.yml, .yaml) File Properties 사용하기

weightle55·2024년 9월 21일
0

Spring boot

목록 보기
3/3

서론 🙌


  • module로 나누어진 프로젝트에만 손댈 수 있는 상황
  • module에서는 우리 API를 호출해서 응답을 처리하는 로직이 들어있다.
  • stage 별로 호출 도메인이 달라 설정파일(properties or yml) 파일로 설정이 필요한 부분이었는데, application.yml에 설정하기에는 해당 module을 import하는 application마다 yml을 수정해주어야 하는 상황
  • module에서 custom yml(예제에서는 extra.yml로 설정해서 사용)에 module에 필요한 properties 지정이 필요했다.

구성 및 Dependencies

Dependencies

  • 간단하게 @Configuration만 사용해보면 되니, Spring boot web과 lombok 만 사용했다.

예제 구성

  • TestController: 테스트 API 호출용 controller
  • TestService : Extra property를 받아 return하는 method가 담긴 서비스
  • ExtPropertiesLoader : extra.yml을 읽어 Environment 객체에 추가하는 Configuration

진행 단계 📌

  1. 단순히 extra.yml을 읽는 방법
  2. extra.yml 한 파일에 프로파일별 분리하여 적용하기
  3. extra.ymlspring.profile.active 값에 따라 extra.yml 을 default로 읽고, 추가로 extra-profile.yml 을 읽어 처리

1. 단순하게 extra.yml 파일 읽기

예제 git (master branch)
https://github.com/weightle55/propertyset/blob/master/src/main/java/com/example/propertyset/configuration/ExtPropertiesLoader.java

# extra.yml
ext:
  test:
    value: localVal
// ExtPropertiesLoader
@Configuration
public class ExtPropertiesLoader implements EnvironmentAware {

  private static final String EXTRA_YML_PATH = "extra.yml";
  private ConfigurableEnvironment environment;

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = (ConfigurableEnvironment) environment;

    // Resource 설정
    Resource resource = new ClassPathResource(EXTRA_YML_PATH);
    YamlPropertySourceLoader loader = new YamlPropertySourceLoader();

    try {
      //Property를 읽고 environment 객체에 추가
      PropertySource<?> yamlProperties = loader.load("extra", resource).get(0);
      this.environment.getPropertySources().addLast(yamlProperties);
    } catch (IOException e) {
      throw new RuntimeException(EXTRA_YML_PATH + " load error", e);
    }
  }
}

왜 그냥 @PropertySource 어노테이션을 사용하지 않았나? 🤔

  • 단순히 특정 Class에서 Property를 사용하고자 했다면 @PropertySource를 이용할 수 있었지만, module에 web-client로 feign client를 사용했는데, feign-client는 타 모듈에서 Bean을 생성해서 가져오는 부분이 있었다.
  • @FeignClient 어노테이션을 사용하여 Client에 url을 설정해줘야하는데 FeignClient에 Bean생성 타임에 해당 값이 없어 문제가 발생하여, Environment에 해당 프로퍼티를 추가하는 방향으로 진행 Loader 객체를 생성했다.(실제는 그렇고 예제는 단순 Spring boot로 생성하여 진행)
@Service
public class TestService {

  @Value("${ext.test.value}")
  private String testValue;

  public String getTestValue() {
    return testValue;
  }
}

서비스에서는 단순하게 Binding된 testValue 값을 리턴

결과

  • ext.test.value에 저장된 local Value가 정상적으로 출력

2. extra.yml 파일에 profile 별로 분리하여 처리하기

예제 git (master feature/read-yml-with-one-file)
https://github.com/weightle55/propertyset/blob/feature/read-yml-with-one-file/src/main/java/com/example/propertyset/configuration/ExtPropertiesLoader.java

# ext.yml
ext:
  test:
    value: localVal

---
on-profile: dev

ext:
  test:
    value: devVal

---
on-profile: release

ext:
  test:
    value: releaseVal
// 참고 - 틀린 클래스이다.
@Configuration
public class ExtPropertiesLoader implements EnvironmentAware {

  private static final String EXTRA_YML_PATH = "extra.yml";

  private ConfigurableEnvironment environment;

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = (ConfigurableEnvironment) environment;

    loadYamlProperties(EXTRA_YML_PATH);
  }

  private void loadYamlProperties(String resourcePath) {
    try {
      Resource resource = new ClassPathResource(resourcePath);
      YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
      // 여러 섹션이 있는 YAML 파일을 로드
      List<PropertySource<?>> yamlProperties = loader.load(resourcePath, resource);

      //profile 가져오기
      List<String> activeProfiles = Arrays.stream(environment.getActiveProfiles()).toList();
      for (PropertySource<?> propertySource : yamlProperties) {
        //기본 섹션 추가
        if (propertySource.getProperty("on-profile") == null) {
          this.environment.getPropertySources().addLast(propertySource);
        }
        else if (activeProfiles.contains(propertySource.getProperty("on-profile"))) {
          this.environment.getPropertySources().addLast(propertySource);
        }
      }

      for (String profile : activeProfiles) {
        System.out.println(profile);
      }
    } catch (IOException e) {
      throw new RuntimeException(resourcePath + " load error", e);
    }
  }
}

엥? 결과가 왜 이래... 😅

  • profile dev로 실행했으나, localVal 값이 리턴됬다.

왜 why? 🤔

  • 나는 보통 application.yml로 사용될 때는 ---로 분할된 application.yml 파일에서 같은 키값(에제에서 ext.test.value) 가 있으면, 새 값으로 덮인다고 생각했다.
    그런데, 디버거를 확인해보니

  • default와 dev properties가 각각 들어갔다.

  • application.yml에 추가할 때는 같은 키 값은 아래의 값으로 덮인다.

  • 해당 부분은 Spring 에서 appliation.yml을 읽을 때, 프로파일 별로 중복 키값을 처리해주는 로직이 있으나, YamlPropertySourceLoader 로 읽을 때는 직접 구현해 주어야 한다.


Map<String, Object> 를 사용하여, 같은 프로퍼티는 덮이도록 변경

// ExtPropertiesLoader.java
@Configuration
public class ExtPropertiesLoader implements EnvironmentAware {

  private static final String EXTRA_YML_PATH = "extra.yml";

  private ConfigurableEnvironment environment;

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = (ConfigurableEnvironment) environment;

    loadYamlProperties(EXTRA_YML_PATH);
  }

  private void loadYamlProperties(String resourcePath) {
    try {
      Resource resource = new ClassPathResource(resourcePath);
      YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
      // 여러 섹션이 있는 YAML 파일을 로드
      List<PropertySource<?>> yamlProperties = loader.load(resourcePath, resource);

      //profile 가져오기
      List<String> activeProfiles = Arrays.stream(environment.getActiveProfiles()).toList();
      // 값을 덮기 위해 Properties를 저장할 Map 생성
      Map<String, Object> mergedProperties = new HashMap<>();
      for (PropertySource<?> propertySource : yamlProperties) {
        //기본 섹션 추가
        if (propertySource.getProperty("on-profile") == null) {
          mergedProperties.putAll((Map<String,Object>) propertySource.getSource());
        }
        // profiles에 "on-profile"과 일치하는 값이 있을 때만 읽어서 맵에 추가
        else if (activeProfiles.contains(propertySource.getProperty("on-profile"))) {
          mergedProperties.putAll((Map<String,Object>) propertySource.getSource());
        }
      }

      this.environment.getPropertySources().addLast(new MapPropertySource("extraProperties",mergedProperties));

    } catch (IOException e) {
      throw new RuntimeException(resourcePath + " load error", e);
    }
  }
}

Map<String, Object> mergedProperties를 생성하여, default에 포함된 키들을 먼저 넣고, 해당 프로파일의 키가 덮히도록 변경했다.

결과

spring.profiles.active에 설정된 대로 잘 나왔다.


3. extra-profile.yml 로 분리하기

참고 git (branch - feature/dividing-files)
https://github.com/weightle55/propertyset/blob/feature/dividing-files/src/main/java/com/example/propertyset/configuration/ExtPropertiesLoader.java

profile 분류

default : extra.yml -> 기본으로 읽는 yml
dev : extra-dev.yml -> spring.profiles.active=dev 면 읽는 yml
release : extra-release.yml -> spring.profiles.active=release 면 읽는 yml

# ext.yml
ext:
  test:
    value: localVal

# ext-dev.yml
ext:
  test:
    value: devVal
    
# ext-release.yml
ext:
  test:
    value: releaseVal
// ExtPropertiesLoader
@Configuration
public class ExtPropertiesLoader implements EnvironmentAware {

  private static final String YML_FILE_NAME = "extra";

  private ConfigurableEnvironment environment;

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = (ConfigurableEnvironment) environment;

    loadYamlFileByProfile();
  }

  private void loadYamlFileByProfile() {
    try {
      //defaultResource 가져오기
      Resource defaultResource = new ClassPathResource(YML_FILE_NAME + ".yml");
      YamlPropertySourceLoader loader = new YamlPropertySourceLoader();

      // default yml 읽기
      List<PropertySource<?>> yamlProperties = loader.load("extra", defaultResource);
      Map<String, Object> mergedProperties = new HashMap<>();
      // extra.yml 파일이 있다면 초기화
      if (!yamlProperties.isEmpty()) {
        mergedProperties.putAll((Map<String, Object>) yamlProperties.get(0).getSource());
      }

      //Active Profile 가져오기
      String[] activeProfiles = environment.getActiveProfiles();
      for (String profile : activeProfiles) {
        //extra-profile.yml 읽기
        Resource profileResource = new ClassPathResource(YML_FILE_NAME + "-" + profile + ".yml");
        List<PropertySource<?>> profileYamlProperties = loader.load("extra", profileResource);

        // profile 설정이 되어있고, profile file이 있으면, property Map의 같은 키값 덮기
        if(!profileYamlProperties.isEmpty()) {
          mergedProperties.putAll((Map<String, Object>) profileYamlProperties.get(0).getSource());
        }
      }
      this.environment.getPropertySources().addLast(new MapPropertySource("extraProperties", mergedProperties));
    } catch (IOException e) {
      throw new RuntimeException(YML_FILE_NAME + " load error", e);
    }
  }
}

2번에서 겪은 대로 Map을 사용하여, 중복 처리 적용

mergedProperties.putAll((Map<String, Object>) yamlProperties.get(0).getSource());

ReadFile Name="extra.yml" 대신 profile을 읽어 합쳐서 처리하도록 변경했다.

for (String profile : activeProfiles) {
        //extra-profile.yml 읽기
        Resource profileResource = new ClassPathResource(YML_FILE_NAME + "-" + profile + ".yml");
        List<PropertySource<?>> profileYamlProperties = loader.load("extra", profileResource);

        // profile 설정이 되어있고, profile file이 있으면, property Map의 같은 키값 덮기
        if(!profileYamlProperties.isEmpty()) {
          mergedProperties.putAll((Map<String, Object>) profileYamlProperties.get(0).getSource());
        }
      }

결과

  • spring.profiles.active 없음
  • spring.profiles.active=dev
  • spring.profiles.active=release

0개의 댓글