YAML로 프로퍼티 작성

yanju·2023년 9월 27일
0
post-thumbnail

YAML

YAML이라는 이름은 "YAML은 마크업 언어가 아니다 (YAML Ain't Markup Language)” 라는 재귀적인 이름에서 유래되었다.

원래 YAML의 뜻은 “또 다른 마크업 언어 (Yet Another Markup Language)”였으나, YAML의 핵심은 문서 마크업이 아닌 데이터 중심에 있다는 것을 보여주기 위해 이름을 바꾸었다.

오늘날 XML과 JSON이 데이터 직렬화에 주로 쓰이기 시작하면서, 많은 사람들이 YAML을 '가벼운 마크업 언어'로 사용하려 하고 있다.

Spring에서 YAML 사용

기존 properties 파일 대신 yaml 파일 작성

kdt:
  version: "1.0"
  minimum-order-amount: 1
  support-vendors:
    - a
    - b
    - c
    - d
  description: |
    line 1 hello world
    line 2 xxxx
    line 3

Configuration 빈에서 @PropertySource 변경 properties → yaml

//@PropertySource("application.properties")
@PropertySource("application.yaml")
public class AppConfiguration {
}

yaml 파일 읽어서 확인하는 실행 파일 작성

public class OrderTester {

    public static void main(String[] args) {
        var applicationContext = new AnnotationConfigApplicationContext(AppConfiguration.class);

        var environment = applicationContext.getEnvironment();
        var version = environment.getProperty("kdt.version");
        var minimumOrderAmount = environment.getProperty("kdt.minimum-order-amount", Integer.class);
        var supportVendors = environment.getProperty("kdt.support-vendors", List.class);
        var description = environment.getProperty("kdt.description", List.class);
        System.out.println("version = " + version);
        System.out.println("minimumOrderAmount = " + minimumOrderAmount);
        System.out.println("supportVendors = " + supportVendors);
        System.out.println("description = " + description);

        applicationContext.close();
    }

}

실행 후 오류 발생

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderProperties': Unsatisfied dependency expressed through field 'minimumOrderAmount'; nested exception is org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "${kdt.minimum-order-amount}"
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:660)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1413)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
	at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:93)
	at org.prgrms.kdt.OrderTester.main(OrderTester.java:21)
Caused by: org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "${kdt.minimum-order-amount}"
	at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:79)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1328)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:657)
	... 15 more
Caused by: java.lang.NumberFormatException: For input string: "${kdt.minimum-order-amount}"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:654)
	at java.base/java.lang.Integer.valueOf(Integer.java:999)
	at org.springframework.util.NumberUtils.parseNumber(NumberUtils.java:211)
	at org.springframework.beans.propertyeditors.CustomNumberEditor.setAsText(CustomNumberEditor.java:115)
	at org.springframework.beans.TypeConverterDelegate.doConvertTextValue(TypeConverterDelegate.java:429)
	at org.springframework.beans.TypeConverterDelegate.doConvertValue(TypeConverterDelegate.java:402)
	at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:155)
	at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73)
	... 18 more

스프링부트는 yaml을 기본적으로 지원하지만, 스프링 프레임워크는 기본적으로 지원하지 않는다.

따라서 스프링에서 yaml 파일은 @PropertySource로 읽을 수 없다.

지원하지 않는 소스는 PropertySourceFactory를 직접 구현해 읽는다.

public class YamlPropertiesFactory implements PropertySourceFactory {

    @Override
    public PropertySource<?> createPropertySource(String s, EncodedResource encodedResource) throws IOException {
        var yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
        yamlPropertiesFactoryBean.setResources(encodedResource.getResource());

        var properties = yamlPropertiesFactoryBean.getObject();
        return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
    }

}

Configuration 빈의 @PropertySource에도 factory속성을 추가해 yaml을 읽도록 변경한다

@PropertySource(value = "application.yaml", factory = YamlPropertiesFactory.class)
public class AppConfiguration {
}

이제 실행을 하면 다음과 같은 결과가 나온다.

version = 1.0
minimumOrderAmount = 1
supportVendors = null
description = [line 1 hello world
line 2 xxxx
line 3]

나머지 값들은 잘 나오지만 리스트 형식인 supportVendros는 실제 값을 못 가져오고 null로 됐다.

yaml의 리스트 값을 읽기 위해서는 스프링부트에서 제공하는 Configuration Properties를 사용해야 한다.

@ConfigurationProperties를 이용하면 yaml에 있는 값을 객체로 매핑할 수 있다.

이전에 만들어 둔 OrderProperties 클래스를 사용해 yaml 값을 매핑한다.

properties 파일을 매핑할 때 쓴 @Value 어노테이션을 사용하지 않아도 된다.

@Configuration Propertiesprefix속성을 yaml의 prefix인 kdt와 맞춰야 한다.

또한 값을 매핑할 필드들에 대해서 getter, setter도 필요하다.

@Configuration
@ConfigurationProperties(prefix = "kdt")
public class OrderProperties implements InitializingBean {

    private String version;
    private int minimumOrderAmount;
    private List<String> supportVendors;
    private String description;

    @Value("${JAVA_HOME}")
    private String javaHome;

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("OrderProperties.afterPropertiesSet");
        System.out.println("version = " + version);
        System.out.println("minimumOrderAmount = " + minimumOrderAmount);
        System.out.println("supportVendors = " + supportVendors);
        System.out.println("description = " + description);
        System.out.println("javaHome = " + javaHome);
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public int getMinimumOrderAmount() {
        return minimumOrderAmount;
    }

    public void setMinimumOrderAmount(int minimumOrderAmount) {
        this.minimumOrderAmount = minimumOrderAmount;
    }

    public List<String> getSupportVendors() {
        return supportVendors;
    }

    public void setSupportVendors(List<String> supportVendors) {
        this.supportVendors = supportVendors;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

@ConfigurationProperties는 스프링부트에서 지원하므로 사용하기 위해 Configuration 파일에 @EnableConfigurationProperties 어노테이션을 추가해야한다.

@Configuration
@ComponentScan(basePackages = {"org.prgrms.kdt.order", "org.prgrms.kdt.voucher", "org.prgrms.kdt.configuration"})
@PropertySource(value = "application.yaml", factory = YamlPropertiesFactory.class)
@EnableConfigurationProperties
public class AppConfiguration {

}

이제 실행 파일에서 OrderProperties빈을 가져와 결과를 확인한다.

public class OrderTester {

    public static void main(String[] args) {
        var applicationContext = new AnnotationConfigApplicationContext(AppConfiguration.class);

				var orderProperties = applicationContext.getBean(OrderProperties.class);
        System.out.println("version = " + orderProperties.getVersion());
        System.out.println("minimumOrderAmount = " + orderProperties.getMinimumOrderAmount());
        System.out.println("supportVendors = " + orderProperties.getSupportVendors());
        System.out.println("description = " + orderProperties.getDescription());

        applicationContext.close();
    }

}

이제 결과는 리스트 형식인 supportVendors를 포함해 다음과 같이 잘 출력 된다.

코드

https://github.com/yanJuicy/kdt-spring-order/tree/342a4108cf3fbd3f597daed543b739f4773188dc

0개의 댓글