Dev./Spring

[Spring] Configuration 비교

hotpotato0 2022. 5. 18. 22:33

Springboot 설정 파일 yaml 사용법

대부분의 애플리케이션에서 설정(Configuration)과 관련된 변수(정보)들은 보통 파일에 쓰고 읽는 방식으로 프로그래밍함.

외부에 설정파일을 넣을 수도 있고, 내부적으로 프로젝트에 넣을 수도 있다. 파일 포맷 또한 다양하다(.properties, .ini)

Springboot 에서도 이런 Configuration에 대한 내용을 다양한 포맷의 파일에 적고, 읽기가 가능한데, 그중 가장 적합하고 권장하는 방식인 yaml에 대해 간단히 정리해보자

*.properties vs *.yml

[properties]

spring.datasource.hikari.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.hikari.jdbc-url=jdbc:mariadb://localhost:3306/testdb
spring.datasource.hikari.username=root
spring.datasource.hikari.password=root

spring.datasource.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.datasource.jpa.properties.hibernate.format_sql=true

spring.datasource.jpa.show-sql=true
spring.datasource.jpa.generate-ddl=true

 

[yaml]

spring:
  datasource:
    hikari:
      driver-class-name: org.mariadb.jdbc.Driver
      jdbc-url: jdbc:mariadb://localhost:3306/testdb
      username: root
      password: root
    jpa:
      properties:
        hibernate:
          dialect: org.hibernate.dialect.MySQL5InnoDBDialect
          format_sql: true
      show-sql: true
      generate-ddl: true

 

두개를 비교해서 보면 어떤가? 바로 어떤걸 골라야할지 이유가 나온다. -> yaml

Why YAML?

-> 보기 편함. 똑같은 설정이지만 yaml을 사용하면 위와 같이 들여쓰기(띄어쓰기)로 구분하여 계층구조로 표현이 가능, 사람이 보기가 편하다. 즉 관리가 편하다.(prefix의 중복 제거 가능)

또 이를 리스트로 표현하고자 할때는 '-'를 추가하여 쓸수 있다.

my:
servers:
    - dev.example.com
    - another.example.com

또 보통 local, dev, qa, prod 라는 환경을 구분하여 개발을 진행하는데 이때 .properties는 모든 환경마다 별도로 구성을 해줘야하지만, 

.yaml는 한 파일에 해당 profile을 지정해서 설정을 다르게 할 수 있다는 장점이 있다.

server:
    address: 192.168.1.100
---
spring:
    profiles: development
server:
    address: 127.0.0.1
---
spring:
    profiles: production & eu-central
server:
    address: 192.168.1.120

 

properties 값 주입 받기

 앞서 말한 yaml을 활용할수 있는 것중 하나가 값 주입이다.

 

@Value

 가장 간단하게 값을 주입하는 방식이다. 사용방법은 @Value 어노테이션에 값을 가리키는 placeholder 를 명시해주거나 SpEL(Spring Expression Language)을 명시하면 된다.

 

sample) application.yml

external:
  record-year: 2022
  api:
    name: sangdon
    key: 123123

 

placeholder 방식

${}를 활용하여 내부에 값의 위치를 적어 @Value에 값을 주입하는 방식이다.

 

@Service
public class ExternalService {
    @Value("${external.record-year}")
    private String recordYear;
  
    @Value("${external.api.name}")
    private String apiName;
  
    @Value("${external.api.key}")
    private Integer apiKey;
}

이런 방식을 통해 String, Integer 등의 타입 값을 주입하여 사용할 수 있다.

 

SpEL 방식

@SpringBootTest
public class SpELTest {

    @Value("#{1 eq 1}")
    private boolean spelBoolean;

    @Value("#{externalService.apiName eq 'kakao'}")
    private String spelNameString;

    @Test
    void spelTest() {
        assertThat(spelBoolean).isTrue();
        assertThat(spelNameString).isEqualTo("true");
    }
}

 

문제점

@Value 방식에는 안타깝게도 문제점이 있다.

위 예제에서 true 값이 String으로도 Boolean으로도 사용되는 것을 확인 할 수 있다.
다시한번 코드로 살펴보도록 하자.

타입 안정성

아래와 같은 application.yml이 있고 개발자는 이 값을 boolean으로 사용하길 바랬다고 가정하자.

external:
  value: true

그리고 아래 테스트 코드는 성공을 한다.

@SpringBootTest
public class ValueProblemTest {
    @Value("${external.value}")
    private String stringValue;

    @Value("${external.value}")
    private boolean booelanValue;

    @Test
    void problemTest() {
        assertThat(stringValue).isEqualTo("true");
        assertThat(booelanValue).isEqualTo(true);
    }
}

보이는 것과 같이 똑같은 값을 String으로도 boolean으로도 사용할 수 있다. 이게 어떤 문제가 되는 것일까?

.yml에 설정해둔 값을 서로 다른 클래스에서 @Value를 이용해서 산발적으로 사용한다고 생각해보자. 설정값을 boolean으로 가져다 쓴다면 다행이지만 모든 개발자가 String으로 불러다 쓰는 실수하지 않을 거라는 확신을 할 수 없다. 지금은 단순히 true이지만 숫자가 연속된 형태의 값(ex.342462351)이라면 이 값을 숫자로 인지할지 문자열로 인지할지 모르게 된다. 결국 타입에 대한 안정성을 가지기가 힘들다는 것을 의미한다.

 

@ConfigurationProperties

이번에는 타입 안정성을 가지는 방법을 사용해 보도록 하자.

이 방법은 클래스를 정의해 놓고 값을 주입해서 사용한다.

예제 코드로 알아보자.

external:
  record-year: 2020
  api:
    name: kakao
    key: 123123

위와 같은 application.yml이 있을 때

@Getter
@Setter
@Configuration
@ConfigurationProperties("external")
public class TypeSafeProperties {
    private String recordYear;
    private Api api;

    @Getter
    @Setter
    public static class Api {
        private String name;
        private Integer key;
    }
}

위와 같이 Properties 클래스를 정의해서 사용한다.

@ConfigurationProperties  value로 prefix를 적어줘야 한다. 중첩 클래스(ex. Api)를 사용하게 되는 경우 이름을 똑같이 일치시켜줘야 하는 것을 주의해야 한다.

한가지 더 주의해야 할 점은 setter를 반드시 정의해주어야 한다. setter가 없다면 Caused by: java.lang.IllegalStateException: No setter found for property 이 발생하게 된다.

@Value 도 똑같이 할수 있지 않나?

똑같이 할 수 있다. 그러나 불편한 점이 있다.

완성된 코드를 보도록 하자. 위에서 사용했던 application.yml 의 external 값들을 사용했다.

@Getter
@RequiredArgsConstructor
@Configuration
public class ExternalProperties {
    @Value("${external.record-year}")
    private String recordYear;
    private final Api api;

    @Getter
    @Configuration
    public static class Api {
        @Value("${external.api.name}")
        private String apiName;
        @Value("${external.api.key}")
        private Integer apiKey;
    }
}

@Value에 계속에서 중복된 값(external.api)을 적어줘야 하는 불편함이 있고 중첩 클래스를 Bean으로 생성해서 의존성 주입을 해줘야 한다.

그리고 @Value에서는 Snake case로 작성된 값들을 토씨 하나 틀리지 않고 적어줘야 했지만 @ConfigurationProperites를 사용하면 Camel case로 작성한 변수를 찾아 주입해준다.

문제점

불변성

@Value와 @ConfigurationProperties 두 방식 모두 공통으로 불변이 아니라는 문제점이 있다.

문제는 final 한 필드로 인스턴스 변수를 생성할 수 없기 때문에 불변성을 유지할 수가 없다. 심지어 @ConfigurationProperties은 개발자 입장에서 불필요한 setter가 공개되어 있어 중간에 값이 변경될 위험성이 크게 남아있다.

@ConstructorBinding

이전에는 불변성을 지킬 수 없는 문제가 있었기 때문에 Spring Boot 2.3 버전 이후 생성자 주입방식으로 불변성을 가지고 Properties 파일을 만들 수 있는 방식이 추가되었다.

@ConstructorBinding 어노테이션을 이용하면 final 필드에 대해 값을 주입해준다. 그리고 중첩 클래스가 있다면 자동으로 중첩 클래스의 final 필드 또한 자동으로 값을 주입하는 대상이 된다.

final 키워드를 명시하지 않는다면 setter를 이용해서 값을 binding 하려하기 때문에 setter가 없다는 exception이 발생한다.

@Getter
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties("external")
public final class ConstructorProperties {
    private final String recordYear;
    private final Api api;

    @Getter
    @RequiredArgsConstructor
    public static final class Api {
        private final String name;
        private final Integer key;
    }
}

다만 이 방식을 사용하면 Properties 클래스에 직접적으로 @Configuration을 이용해서 직접적으로 Spring Bean으로 만들어 주지 않는다.

대신 아래와 같이 PropertiesConfiguration 클래스에 @EnableConfigurationProperties을 이용해서 생성할 Properties 클래스의 클래스 타입을 명시해주면 Spring Bean으로 등록된다.

@Configuration
@EnableConfigurationProperties(value = {ConstructorProperties.class})
public class PropertiesConfiguration {
}

이렇게 @ConstructorBinding 어노테이션을 이용함으로써 불필요한 setter를 사용하지 않게 되면서 불변성을 유지할 수 있게 됐다.

정리

@Value를 사용하여 값을 주입하는 방식과 @ConfigurationProperties를 이용해서 외부 설정 파일에 존재하는 값을 주입하는 방법을 알아봤다.

@Value와 @ConfigurationProperties를 이용하는 방식에 대한 차이는 Spring Boot 공식 문서에서 잘 설명이 되어 있다. 해당 부분을 전부 다루기에는 글이 너무 길어지기 때문에 가장 눈에 띄는 차이에 대해서 몇가지 알아봤다.

가급적이면 불변성을 유지할 수 있는 @ConfigurationProperties과 @ConstructorBinding 을 같이 사용하는 것이 좋아 보이지만 Spring Batch를 사용하면 Late Binding을 위해 @Value를 사용해야 하는 경우도 발생한다. 따라서 언제나 그렇듯 상황에 따라 알맞게 적절한 어노테이션을 선택해서 사용해야 한다.