배경
현재 개발하고 있는 것은 ‘서비스의 정산’에 대한 서비스이다. 그런데 정산이라는 것이 결제가 있다면 항상 들어가야 하는 미들웨어와 같은 성격을 지니고 있다.그러다보니 서비스의 설정 정보들이 각 프로젝트에 혼재해있어서 정리를 해보려고 한다.
현재 상황
전체적으로 정산에 대한 공통 부분을 사용하기 위해 settlement-common
이라는 모듈을 사용하고 있다. 각 서비스에서는 settlement-common
에 대한 의존성을 연결하여 사용한다. 그런데 정산에서 사용하는 property 정보들이 모두 하위 서비스에서 정의하여 사용하고 있다. 예를 들면 erp.url
은 하위 서비스의 application.yml
에 정의되어 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
spring: profiles: default --- spring: profiles: default erp: url: "https://localhost" --- spring: profiles: production erp: url: "https://erp-server" --- |
하고 싶은 것
보통의 경우에는 common 공통의 설정을 로딩해서 사용하고, 일부의 케이스에 대해서만 하위 서비스에서 설정을 overriding하고 싶었다. application.yml
에 다른 설정과 함께 섞여 있는 것도 제거하고 싶었다.
@ConfigurationProperties 사용
아래처럼 Property정보를 Bean으로 등록해서 사용하는 것이 @Value
를 사용하는 것보다 나을 거란 생각을 했다.
@Component
가 없어도 Bean으로 사용할 수 있는데 굳이 쓴 이유는, IDE에서 Bean Mapping이 안된다는 waring이 발생하는 게 싫어서이다. 그리고 @ConfigurationProperties
는 setter를 이용해서 값을 주입하는 것도 약간은(?) 신기한 부분이다. 최근 스프링의 추세가 reflection을 통한 강제 주입이라서.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "settlement") public class SettlementProperties { @Getter @Setter private Erp erp; public static class Erp { @Getter @Setter String url; } } |
파일분리 (application.yml -> settlement.yml)
애사당초 얘기했던 것처럼, 하위 프로젝트의 application.yml
에서 모든 것을 정의해서 사용하면, 어떤 property를 어떤 모듈에서 사용하는지 파악하기 힘들고, 이후에 리팩토링할 때에도 장애물이 된다. 그래서 @ConfigurationProperties
의 locations
attribute를 활용하여 파일을 분리하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "settlement", locations = "classpath:/settlement.yml) public class SettlementProperties { @Getter @Setter private Erp erp; public static class Erp { @Getter @Setter String url; } } |
locations attribute deprecated
그런데 locations
attribute가 deprecate되었다. 심지어 최신 버전의 springboot에서는 아예 제거되었다. 1 그래서 별로 권장하지 않는다고는 하지만, @PropertySource
를 활용하기로 했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "settlement") @PropertySource(value = {"classpath:/settlement.yml"}) public class SettlementProperties { @Getter @Setter private Erp erp; public static class Erp { @Getter @Setter String url; } } |
spring.active.profile 기능 활용
아래처럼 springEL을 활용해서 각 profile별로 필요한 설정을 import할 수 있다. 다만 같은 값의 overriding의 경우 선언한 순서에 영향을 받는다. 2 특히 아래와 같은 경우에는 @PropertySource
에 선언된 Array 중에 나중에 선언된 것이 override된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "settlement") @PropertySource(value = { "classpath:/settlement.yml", "classpath:/settlement-${spring.profiles.active}.yml" }) public class SettlementProperties { @Getter @Setter private Erp erp; public static class Erp { @Getter @Setter String url; } } |
문제 발생, yml 파싱 불가
위처럼 로직을 짜고 테스트를 돌려보면, NPE가 발생한다.
이유인즉슨, Bean이 제대로 setting이 되지 않았기 때문인데, 디버거를 통해 확인했더니 yml 파일을 인식하지 못한다. 3 그래서 yml 파일을 사용할 수 있도록 Factory 클래스를 추가 구현하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "settlement") @PropertySource(value = { "classpath:/settlement.yml", "classpath:/settlement-${spring.profiles.active}.yml" }, factory = YamlPropertySourceFactory.class) public class SettlementProperties { @Getter @Setter private Erp erp; public static class Erp { @Getter @Setter String url; } } public class YamlPropertySourceFactory implements PropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException { return name != null ? new YamlPropertySourceLoader().load(name, resource.getResource(), null) : new YamlPropertySourceLoader().load( getNameForResource(resource.getResource()), resource.getResource(), null); } private static String getNameForResource(Resource resource) { String name = resource.getDescription(); if (!StringUtils.hasText(name)) { name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource); } return name; } } |
yml 파싱은 잘되는데, profile은 어떻게…
application.yml
처럼 Array 형식으로 정의하고 profile별로 property를 꺼내 쓰고 싶은데, 위의 YamlPropertySourceFactory
를 보면, profile을 null로 넘겼다. 해당 profile을 넣어주면 제대로 파싱될 것 같은데, Factory 파일이 bean도 아니라서 Environment
를 사용할 수 없고 어떻게 해야할지는 좀더 생각해봐야 할 것 같다.
하위 서비스에서 설정 완료
하위 서비스에서는 아래처럼 추가 설정을 overriding해서 사용할 수 있다.
1 2 3 4 5 6 7 8 9 |
@Configuration @PropertySource(value = { "classpath:/settlement.yml", "classpath:/settlement-${spring.profiles.active}.yml", "classpath:/settlement-admin.yml", "classpath:/settlement-admin-${spring.profiles.active}.yml" }, factory = YamlPropertySourceFactory.class) public class AdminSettlementConfig { } |
또 다른 문제, 파일이 없을 경우
그런데 settlement-common
은 하위 프로젝트들의 모든 spring.profiles
를 알 수 없다. 그래서 정의되지 않은 profile이 들어올 경우, 리소스를 읽을 수 없다면서 settlement-${spring.profiles.active}.yml
구문에서 오류가 발생한다. 그래서 아래처럼 @PropertySource
를 분리하고, ignoreResourceNotFound
속성을 추가해서 처리하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "settlement") @PropertySources({ @PropertySource(value = {"classpath:/settlement.yml"}, factory = YamlPropertySourceFactory.class), @PropertySource(value = {"classpath:/settlement-${spring.profiles.active}.yml"}, ignoreResourceNotFound = true, factory = YamlPropertySourceFactory.class) }) public class SettlementProperties { @Getter @Setter private Erp erp; public static class Erp { @Getter @Setter String url; } } |
그런데 계속 오류가 발생한다. 디버거를 통해서 확인하였더니, YamlPropertySourceFactory
내에서 YamlPropertySourceLoader
를 사용하고 있는데, 여기에서 리소스가 없는 경우에 대해 기존의 경우와 다르게 return을 하고 있다. 결국 NPE가 발생한다. (기본 factory를 사용하는 경우에는 문제없음)
결론
yml 포맷이 훨씬 구조화된 형식이라 properties 파일보다 권장되기도 하는데, 사용하려니 현실적인 벽에 많이 부딪혀서 properties 파일을 우선 사용하는 것으로 선회하였다. 끝까지 고쳐보지 못하고, 실패를 결론으로 기록하려니 좀 아쉽긴 한데, 시간도 시간인지라… 나중에는 다시 볼 일이 생기지 않을까 싶은 생각으로 일단 여기까지 기록해둔다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "settlement") @PropertySources({ @PropertySource(value = {"classpath:/settlement.properties"}), @PropertySource(value = {"classpath:/settlement-${spring.profiles.active}.properties"}, ignoreResourceNotFound = true) }) public class SettlementProperties { @Getter @Setter private Erp erp; public static class Erp { @Getter @Setter String url; } } @Configuration @PropertySources({ @PropertySource(value = {"classpath:/settlement.properties"}), @PropertySource(value = {"classpath:/settlement-${spring.profiles.active}.properties"}, ignoreResourceNotFound = true), @PropertySource(value = {"classpath:/settlement-admin.properties"}), @PropertySource(value = {"classpath:/settlement-admin-${spring.profiles.active}.properties"}, ignoreResourceNotFound = true) }) public class AdminSettlementConfig { } |
추가1
resource 파일은 classpath로 복사될 때, 같은 이름이면 우선순위에 따라 overwrite된다. 즉, application.properties
또는 application.yml
파일이 settlement-common
에 있는데, 하위 모듈에서 동일한 파일이 있는 경우에는 내용에 상관없이 하위 모듈의 것을 사용하게 된다. 심지어 해당 property가 없는 경우라 할지라도 common의 것을 사용할 수 없다. override가 아니라 파일이 overwrite되는 것에 유의!
1 2 3 4 5 6 7 |
(settlement-common) application.properties settlement.erp.url=https://common (admin) application.properties (결과) 없음 (NPE가 발생하거나... 구현에 따라서) |
추가2
파일이 다르고 키가 같은 property의 경우에는 해당 값을 우선순위에 따라 override한다.
1 2 3 4 5 6 7 8 |
(settlement-common) application.properties settlement.erp.url=https://common (admin) application.properties settlement.erp.url=https://admin (결과) https://admin |
좋은정보 감사합니다 :)!
도움이 되셨다니 기쁩니다, 즐거운 코딩하세요 🙂