kyucumber
전체 글 보기

Spring Boot 2.4 이후 property 설정과 InvalidConfigDataPropertyException

기존 프로젝트 구성을 동일하게 옮겨 Spring Boot 2.6으로 구성했는데 아래와 같이 InvalidConfigDataPropertyException라는 처음보는 예외가 발생했습니다.

org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property 'spring.profiles.include[0]' imported from location 'class path resource [application-domain.yml]' is invalid in a profile specific resource [origin: class path resource [application-domain.yml] - 4:9]

2.4 관련 마이그레이션 문서를 찾아보아도 뭔가 멀티모듈에서 include 하는 방법이나 구조에 대해 딱히 자세하게 설명해둔 문서가 없었습니다.

해당 버전 이후에서는 어차피 동일한 방식을 사용할 것 같아 마이그레이션을 진행하는 김에 쓸데없지만 코드 레벨로 원인을 살펴보았습니다.

모듈 구성

제가 구성한 프로젝트는 멀티모듈로 구성되어 있으며, admin 모듈이 doamin을, doamin이 region-client를 참조하는 아래와 같은 형태로 구성되어 있습니다.

experimentation-platform-admin └──experimentation-platform-domain └──experimentation-region-client

그리고 각각의 모듈의 하위에는 property.yml이 존재하고 있으며 각각 모듈별로 하위 모듈의 property.yml을 include해 사용합니다.

experimentation-platform-admin ├───application.yml └──experimentation-platform-domain ├───application-domain.yml └──experimentation-region-client └───application-region-client.yml

2.4 이전에 흔하게 위와 같이 사용했고 property.yml을 로드해 사용하는 형태로 큰 문제 없이 사용했는데 2.6으로 마이그레이션 이후 첫 문단에 기술한 아래 예외가 발생했습니다.

org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property 'spring.profiles.include[0]' imported from location 'class path resource [application-domain.yml]' is invalid in a profile specific resource [origin: class path resource [application-domain.yml] - 4:9]

위에서 발생한 예외인 InvalidConfigDataPropertyException(2.6.x)는 2.4.x 이후에 추가된 클래스입니다.

Spring Boot Github에 들어가 보면 아래처럼 2.3.x 이전 버전에는 아예 클래스가 존재하지 않습니다.

Untitled

InvalidConfigDataPropertyException 예외 발생 원인

위와 같은 예외가 발생하는 원인을 파악하기 이전에 각각 모듈 별 property를 살펴보겠습니다.

  1. experimentation-platform-admin/application.yml
spring: profiles: active: local include: - domain - cmdb-api-client - hr-info-client --- spring: config: activate: on-profile: local --- spring: config: activate: on-profile: dev --- spring: config: activate: on-profile: prod
  1. experimentation-platform-domain/application-domain.yml
spring: profiles: include: - region-client --- spring: config: activate: on-profile: local --- spring: config: activate: on-profile: dev --- spring: config: activate: on-profile: prod
  1. clients/experimentation-platform-region-client/application-region-client.yml
feign: client: config: default: connectTimeout: 2000 readTimeout: 2000 loggerLevel: HEADERS --- spring: config: activate: on-profile: local --- spring: config: activate: on-profile: dev --- spring: config: activate: on-profile: prod

딱히 별 특징이 없어 보입니다. 여기서 문제를 발생시킨건 2번 application-domain.yml의 include 부분입니다. 발생한 예외 코드의 Stack trace를 따라가보겠습니다.

  • InvalidConfigDataPropertyException.java

InvalidConfigDataPropertyException 클래스 내부의 throwOrWarn 메소드를 보면 아래와 같은 부분이 있습니다. 아래 코드에서는 isFromProfileSpecificImport가 true인 경우 PROFILE_SPECIFIC_ERRORS를 순회하며 해당하는 property가 존재하는 경우 예외를 발생시키고 있습니다.

if (contributor.isFromProfileSpecificImport() && !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES)) { PROFILE_SPECIFIC_ERRORS.forEach((name) -> { ConfigurationProperty property = propertySource.getConfigurationProperty(name); if (property != null) { throw new InvalidConfigDataPropertyException(property, true, null, contributor.getResource()); } }); }

isFromProfileSpecificImport가 true가 되는 경우가 어떤 경우인지는 아래 코드를 통해 확인할 수 있습니다.

  • ConfigDataLocationResolvers.java
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolver<?> resolver, ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) { List<ConfigDataResolutionResult> resolved = resolve(location, false, () -> resolver.resolve(context, location)); if (profiles == null) { return resolved; } List<ConfigDataResolutionResult> profileSpecific = resolve(location, true, () -> resolver.resolveProfileSpecific(context, location, profiles)); return merge(resolved, profileSpecific); }

실행할 때 잠시 디버깅을 걸어서 데이터를 확인해보면 application-domain.yml의 profileSpecific 값은 true가 된 것을 확인할 수 있습니다.

Untitled

include 되는 profile의 경우 profileSpecific 값이 true가 됩니다. 여기서 왜 예외가 발생했는지를 알 수 있습니다.

domain은 admin에서 include하는 property이기 때문에 fromProfileSpecificImport 값이 true가 되며 domain에서 regionc-client를 include하게 되는 경우 PROFILE_SPECIFIC_ERRORS에 포함된 아래 조건에 의해 예외가 발생합니다.

public static final String INCLUDE_PROFILES_PROPERTY_NAME = "spring.profiles.include"; static final ConfigurationPropertyName INCLUDE_PROFILES = ConfigurationPropertyName .of(Profiles.INCLUDE_PROFILES_PROPERTY_NAME); errors.add(Profiles.INCLUDE_PROFILES); errors.add(Profiles.INCLUDE_PROFILES.append("[0]"));

사실 코드를 볼 필요 없이 에러 메시지만 보고 include 관련 부분을 수정했다면 바로 해결되었을 문제입니다. 왜 저런 validation이 추가되었는지 궁금했고, 어느 기준으로 해당 부분이 동작하는지를 이해하기 위해 코드를 살펴보았습니다.

해결 방법

위에도 잠시 적어두었지만 domain에 존재하는 include를 제거하면 심플하게 해결할 수 있습니다.

위와 같이 여러 계층 구조를 가진 멀티모듈 구조에서 include를 하는 경우 복잡도가 높아질 수 있어 해당 부분을 막은건지 어떤 의도로 해당 부분이 추가되었는지는 정확히 파악할 순 없지만 저는 아래와 같이 include 설정을 변경해 해결했습니다.

  • experimentation-platform-admin/application.yml
spring: profiles: active: local include: - domain - region-client - cmdb-api-client - hr-info-client

최상위 모듈이 최하위 모듈을 직접 include하게 변경했습니다.

지금처럼 모듈이 다중으로 중첩된 구조는 그리 좋은 구조가 아니라고 생각합니다.

중첩되는 경우 최상위에서 include 하는 것을 잊게되면 여러가지 문제가 발생할 수 있어 이후에는 가급적 아래 같은 2중 중첩 구조는 사용하지 않으려고 생각하고 있습니다.

experimentation-platform-admin └──experimentation-platform-domain └──experimentation-region-client

그리고 마이그레이션을 진행하면서 문서에서 include를 group으로 대체할 수 있다는 것 처럼 기술해두어서 혼란을 겪었습니다.

영어를 잘못 읽어서 마이그레이션 문서를 제대로 파악하지 못한건가 싶은데, 저는 멀티 모듈 구조에서는 include를 사용하지 않고는 다른 모듈의 property.yml이 참조되지 않았습니다.

저는 최종적으로 아래와 같은 형태로 멀티모듈에서 include를 하도록 변경하고 마이그레이션을 마무리했습니다.

  1. experimentation-platform-admin/application.yml
spring: profiles.active: local profiles.include: - domain - cmdb-api-client - hr-info-client - region-client --- spring: config: activate: on-profile: local --- spring: config: activate: on-profile: dev --- spring: config: activate: on-profile: prod
  1. experimentation-platform-domain/application-domain.yml
spring: flyway: enabled: false datasource: driver-class-name: org.mariadb.jdbc.Driver hikari: max-lifetime: 56000 connection-timeout: 2000 sql: init: mode: never jpa: hibernate: ddl-auto: validate open-in-view: false properties: hibernate: globally_quoted_identifiers: true globally_quoted_identifiers_skip_column_definitions: true default_batch_fetch_size: 500 query: in_clause_parameter_padding: true --- spring: config: activate: on-profile: local --- spring: config: activate: on-profile: dev --- spring: config: activate: on-profile: prod
  1. clients/experimentation-platform-region-client/application-region-client.yml
feign: client: config: default: connectTimeout: 2000 readTimeout: 2000 loggerLevel: HEADERS --- spring: config: activate: on-profile: local --- spring: config: activate: on-profile: dev --- spring: config: activate: on-profile: prod

위와 같이 설정했을 때 의도한 대로 환경 별 설정을 잘 읽어왔습니다.

저도 코드를 간단하게 살펴본거라 잘못 알았거나, 설정이 이상한 부분이 있을 수 있습니다. 혹시 좋은 방법이 있거나 잘못된 부분이 있다면 댓글로 알려주세요.

Reference