본문 바로가기

Spring Boot

@Value와 @ConfigurationProperties

728x90

Spring에서는 외부 설정을 읽기 위한 방법으로 아래와 같이 다양한 방법을 제공합니다.

  • Environment
  • @Value - 값 주입
  • @ConfigurationProperties - 타입 안전한 설정 속성

이번 포스팅은 각 방법을 어떻게 사용하는지에 대해서 작성해 보았습니다.


application.properties


my.data.url=local.db.com
my.data.username=local_user
my.data.password=local_pw
my.data.etc.max-connection=1
my.data.etc.timeout=3500ms
my.data.etc.options=CACHE,ADMIN

 

위의 properties는 dash(-)를 사용하는 캐밥 표기법을 주로 사용합니다. (max-connection) 이렇게 사용된 표기법을 자바에서는 낙타 표기법(maxConnection)으로 변환하여 받아야 합니다.


Environment

 MyData.class


import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;

@Slf4j
public class MyData {
    private final String url;
    private final String username;
    private final String password;
    private final int maxConnection; // 낙타 표기법 (camel 표기법)

    private final Duration timeout;
    private final List<String> options;
    public MyData(String url, String username, String password, int
            maxConnection, Duration timeout, List<String> options) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.maxConnection = maxConnection;
        this.timeout = timeout;
        this.options = options;
    }

    @PostConstruct
    public void init() {
        log.info("url={}", url);
        log.info("username={}", username);
        log.info("password={}", password);
        log.info("maxConnection={}", maxConnection);
        log.info("timeout={}", timeout);
        log.info("options={}", options);
    }
}

config 클래스 (MyDataConfig.class)


import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.time.Duration;
import java.util.List;

@Slf4j
@Configuration
public class MyDataConfig {
    private final Environment env;

    public MyDataConfig(Environment env) {
        this.env = env;
    }

    @Bean
    public MyData myData() {
        String url = env.getProperty("my.data.url");
        String username = env.getProperty("my.data.username");
        String password = env.getProperty("my.data.password");
        // Environment.getProperty(key, Type) --> Type으로 형 변환
        int maxConnection = env.getProperty("my.data.etc.max-connection", Integer.class); // 문자 -> 숫자
        Duration timeout = env.getProperty("my.data.etc.timeout", Duration.class); // 문자 -> 기간
        List<String> options = env.getProperty("my.data.etc.options", List.class); // 문자 -> 리스트
        return new MyData(url, username, password, maxConnection, timeout, options);
    }
}

 

메인 함수를 실행하면, 아래 결과를 얻을 수 있습니다.


 


Environment를 사용할 경우

  • application.properties에 필요한 외부 설정을 추가하고, Environment를 통해서 해당 값을 읽으면 향후 외부 설정 방식이 달라져도 애플리케이션 코드를 그대로 유지할 수 있다는 장점이 있습니다. (예를 들어, application.properties에서 커맨드 라인 옵션 인수 혹은 자바 시스템 속성으로 변경)
  • Environment를 직접 주입받고, env.getProperty(key)를 통해서 값을 꺼내는 과정을 반복해야 한다는 단점이 있습니다. 이를 해결하기 위해 Spring은 @Value 방식을 제공합니다.

@Value

MyData.claass를 아래와 같이 @Value를 사용해 변경할 수 있습니다. (필드 주입)

  • ${ }를 사용해서 외부 설정의 key 값을 주면 원하는 값을 주입받을 수 있습니다.

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;

import java.time.Duration;
import java.util.List;

@Slf4j
public class MyData {
    @Value("${my.data.url}")
    private String url;
    @Value("${my.data.username}")
    private String username;
    @Value("${my.data.password}")
    private String password;
    @Value("${my.data.etc.max-connection}")
    private int maxConnection;
    @Value("${my.data.etc.timeout}")
    private Duration timeout;
    @Value("${my.data.etc.options}")
    private List<String> options;

    @PostConstruct
    public void init() {
        log.info("url={}", url);
        log.info("username={}", username);
        log.info("password={}", password);
        log.info("maxConnection={}", maxConnection);
        log.info("timeout={}", timeout);
        log.info("options={}", options);
    }
}

MyDataConfig.class


import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class MyDataConfig {

    @Bean
    public MyData myData() {
        return new MyData();
    }
}

또한 아래와 같이 parameter에 설정 값을 주입받을 수도 있습니다.

MyData.class


import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;
import java.util.List;

@Slf4j
public class MyData {

    private String url;
    private String username;
    private String password;
    private int maxConnection;
    private Duration timeout;
    private List<String> options;

    public MyData(String url, String username, String password, int maxConnection, Duration timeout, List<String> options) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.maxConnection = maxConnection;
        this.timeout = timeout;
        this.options = options;
    }

    @PostConstruct
    public void init() {
        log.info("url={}", url);
        log.info("username={}", username);
        log.info("password={}", password);
        log.info("maxConnection={}", maxConnection);
        log.info("timeout={}", timeout);
        log.info("options={}", options);
    }
}

MyDataConfig.class


import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.List;

@Slf4j
@Configuration
public class MyDataConfig {

    @Bean
    public MyData myData(@Value("${my.data.url}") String url,
                         @Value("${my.data.username}") String username,
                         @Value("${my.data.password}") String password,
                         @Value("${my.data.etc.max-connection}") int maxConnection,
                         @Value("${my.data.etc.timeout}") Duration timeout,
                         @Value("${my.data.etc.options}") List<String> options) {
        return new MyData(url, username,password,maxConnection,timeout,options);
    }
}

 

기본값 설정

만약 key를 찾지 못할 경우 코드에서 기본값을 사용하고 싶다면 아래와 같이 : 뒤에 기본값을 적어주면 됩니다.

  • @Value("${my.data.url:default.db.com}") : my.data.url 가 없을 경우, default.db.com을 사용합니다.

@Value를 사용할 경우

  • Environment의 단점을 극복하였지만, @Value 또한 하나하나 외부 설정 정보의 키 값을 입력받아서 주입받아야 하는 부분이 번거롭습니다. 
  • 설정 파일을 보면 my.data 로 묶여 있는데 이런 부분을 객체로 변환해서 사용할 수 있다면 더 편리하고 좋을 것입니다. 이를 제공하는 기능이 @ConfigurationProperties입니다.

@ConfigurationProperties 

type-safe(타입 안전)한 설정 속성이기 때문에, 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있고, 객체를 통해서 활용할 수 있는 부분들이 많습니다. 먼저 코드를 통해 @ConfigurationProperties를 사용하는 법을 확인해 보겠습니다.

 

MyDataProperties.class

해당 클래스에 application.properties의 속성값을 주입받습니다. 아래는 생성자를 생성하여 주입받았습니다. (생성자 대신, lombok의 @Setter를 사용해서도 주입이 가능합니다. 물론, 생성자를 직접 작성하는 대신 lombok의 @AllArgsConstructor를 사용할 수도 있습니다.)


import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;
import java.util.List;

@Getter
@ConfigurationProperties("my.data") // 객체 묶음
public class MyDataProperties {
    private String url;
    private String username;
    private String password;
    private Etc etc;

    public MyDataProperties(String url, 
                        String username, 
                        String password, 
                        @DefaultValue Etc etc) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.etc = etc;
    }

    @Getter
    public static class Etc{
        private int maxConnection;
        private Duration timeout;
        private List<String> options;

        public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
}

 

 

  • 외부 설정을 주입받을 객체 (MyDataProperties 클래스)를 생성합니다. 그리고 각 필드를 외부 설정의 key 값에 맞추어 준비합니다.
    • @ConfigurationProperties가 있으면 외부 설정을 주입받는 객체라는 뜻입니다.
    • 외부 설정 key의 묶음 시작점인 my.data를 지정합니다.
  • @DefaultValue: 해당 값을 찾을 수 없는 경우 기본값을 사용합니다.
    • @DefaultValue Etc etc : etc를 찾을 수 없을 경우, Etc 객체를 생성하고 내부 값은 비워둡니다. (null, 0)
    • @DefaultValue("DEFAULT") List<String> options : options을 찾을 수 없을 경우 DEFAULT 값을 사용합니다.


MyDataConfig.class


import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(MyDataProperties.class) // 추가!
public class MyDataConfig {

    private final MyDataProperties myDataProperties;

    public MyDataConfig(MyDataProperties myDataProperties) {
        this.myDataProperties = myDataProperties;
    }


    @Bean
    public MyData myData() {
        return new MyData(myDataProperties.getUrl(), 
                myDataProperties.getUsername(),
                myDataProperties.getPassword(),
                myDataProperties.getEtc().getMaxConnection(),
                myDataProperties.getEtc().getTimeout(),
                myDataProperties.getEtc().getOptions());
    }
}
  • @EnableConfigurationProperties를 이용해서 Spring에게 사용할 @ConfigurationProperties를 지정해주어야 합니다. 이렇게 지정함으로써, 해당 클래스는 Spring Bean으로 등록되고 필요한 곳에서 주입받아서 사용할 수 있습니다.
    • private final MyDataProperties myDataProperties를 생성자를 통해 주입받아서 사용합니다.

MyData.class

MyData.class는 위에서 사용한 코드와 동일합니다.


import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;
import java.util.List;

@Slf4j
public class MyData {

    private String url;
    private String username;
    private String password;
    private int maxConnection;
    private Duration timeout;
    private List<String> options;

    public MyData(String url, String username, String password, int maxConnection, Duration timeout, List<String> options) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.maxConnection = maxConnection;
        this.timeout = timeout;
        this.options = options;
    }

    @PostConstruct
    public void init() {
        log.info("url={}", url);
        log.info("username={}", username);
        log.info("password={}", password);
        log.info("maxConnection={}", maxConnection);
        log.info("timeout={}", timeout);
        log.info("options={}", options);
    }
}

 

실행 결과는 동일합니다. ConfigurationProperties는 type-safe 하다고 하였습니다. 만약, maxConnection을 문자열 (e.g. "abc")로 지정하면 실행하면 에러 메시지를 받을 수 있습니다. 즉, 타입이 다르면 오류가 발생하므로 외부 데이터의 타입에 대해서 믿고 사용할 수 있습니다.


@ConfigurationPropertiesScan

지금까지 @ConfigurationProperties를 등록할 때 @EnableConfigurationProperties에 해당 클래스를 직접 명시에서 사용하였습니다. @ConfigurationProperties 가 선언된 클래스를 하나하나 빈으로 지정할 경우 이렇게 직접 명시할 수도 있지만, @ConfigurationProperties를 특정 범위로 자동 등록하고 싶을 때는 @ConfigurationPropertiesScan을 사용하면 편리하게 Bean을 등록할 수 있습니다.


package com.example.springboot.config;

import com.example.springboot.MyData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
// 주석 처리
// @EnableConfigurationProperties(MyDataProperties.class) 
public class MyDataConfig {

    private final MyDataProperties myDataProperties;

    public MyDataConfig(MyDataProperties myDataProperties) {
        this.myDataProperties = myDataProperties;
    }


    @Bean
    public MyData myData() {
        return new MyData(myDataProperties.getUrl(), myDataProperties.getUsername(),
                myDataProperties.getPassword(),
                myDataProperties.getEtc().getMaxConnection(),
                myDataProperties.getEtc().getTimeout(),
                myDataProperties.getEtc().getOptions());
    }
}

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
// 해당 범위 안의 파일들 scan하여
// bean으로 등록
@ConfigurationPropertiesScan("com.example.springboot.config") 
public class SpringbootApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.class, args);
    }

}

 


패키지 구조는 아래와 같습니다.


@ConfigurationProperties 검증

@ConfigurationProperties는 type-safe 하기 때문에, 숫자가 들어가야 하는 부분에 문자가 입력되는 문제와 같이 타입이 맞지 않는 데이터 입력의 문제는 예방할 수 있습니다. 하지만, 숫자의 범위라던가, 문자의 길이 같은 부분은 검증이 어렵습니다. 이를 개발자가 하나하나 검증 코드를 작성하는 방법 대신 Spring에서 자바 빈 검증기 (java bean validation) 기능을 제공합니다.

 

Java bean validation 사용하기

자바 빈 검증기를 사용하려면 build.gradle 파일에 아래의 dependency가 필요합니다. 

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

검증기를 추가해서 MyDataProperties.class 코드를 수정해 보겠습니다.


 

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import org.hibernate.validator.constraints.time.DurationMax;
import org.hibernate.validator.constraints.time.DurationMin;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;

import java.time.Duration;
import java.util.List;

@Getter
@ConfigurationProperties("my.data")
@Validated // 추가!
public class MyDataProperties {
    @NotEmpty
    private String url;
    @NotEmpty
    private String username;
    @NotEmpty
    private String password;
    private Etc etc;

    public MyDataProperties(String url, String username, String password, @DefaultValue Etc etc) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.etc = etc;
    }

    @Getter
    public static class Etc{
        @Min(1)
        @Max(999)
        private int maxConnection;
        @DurationMin(seconds = 1)
        @DurationMax(seconds = 60)
        private Duration timeout;
        private List<String> options;

        public Etc(int maxConnection, Duration timeout, List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
}
  • @NotEmpty url , username , password는 항상 값이 있어야 하는 필수 값이 됩니다.
  • @Min(1) @Max(999) maxConnection : 최소 1 , 최대 999의 값을 허용합니다.
  • @DurationMin(seconds = 1) @DurationMax(seconds = 60) : 최소 1, 최대 60초를 허용합니다.

위에서 설정한 값 이외의 값이 외부 설정에 지정되어 있으면 애플리케이션 로딩 시점에 오류 메시지를 확인할 수 있습니다.

 

이렇게 ConfigurationProperties를 이용하면 외부 설정을 객체로 편리하게 변환해서 사용할 수 있고, 외부 설정의 계층을 객체로 묶어서 표현할 수 있으며, type-safe 및 검증기를 적용할 수 있다는 장점이 있습니다.


참고자료]

김영한, 스프링 부트 - 핵심 원리와 활용", 인프런

728x90
반응형