티스토리 뷰

Spring

[Spring] 외부 설정 방법

hyuuny 2023. 4. 6. 12:26

외부 설정 사용 - Environment

스프링은 Environment를 활용해서 더 편리하게 외부 설정을 읽는 방법들을 제공한다.

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

아래 예제를 통해 외부 설정을 읽는 방법을 알아볼텐데, 예제 코드는 이해를 돕기 위함이므로 실제 DB에 접근하지는 않는다.


MyDataSource

@Slf4j
public class MyDataSource {

    private String url;

    private String username;

    private String password;

    private int maxConnection;

    private Duration timeout;

    private List<String> options;

    public MyDataSource(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);
    }

}
  • url, username, password: 접속 url, 이름, 비밀번호
  • maxConnection: 최대 연결 수
  • timeout: 응답 지연시 타임아웃
  • options: 연결시 사용하는 기타 옵션들

application.properties

my.datasource.url=local.db.com
my.datasource.username=hyuuny
my.datasource.password=password
my.datasource.etc.max-connection=1
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN
  • 외부 속성은 설정 데이터(appliation.properties)를 사용한다.
  • 여기서는 별도의 프로필은 사용하지 않았다. 환경에 따라서 다른 설정값이 필요하다면, 각 환경에 맞는 프로필을 적용하면 된다.

이제 외부 속성을 읽어서 앞서 만든 MyDataSource에 값을 설정하고 스프링 빈으로 등록해보자!

MyDataSourceEnvConfig

@Slf4j
@Configuration
public class MyDataSourceEnvConfig {

    private final Environment env;

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

    @Bean
    public MyDataSource myDataSource() {
        String url = env.getProperty("my.datasource.url");
        String username = env.getProperty("my.datasource.username");
        String password = env.getProperty("my.datasource.password");
        int maxConnection = env.getProperty("my.datasource.etc.max-connection", Integer.class);
        Duration timeout = env.getProperty("my.datasource.etc.timeout", Duration.class);
        List<String> options = env.getProperty("my.datasource.etc.options", List.class);
        return new MyDataSource(url, username, password, maxConnection, timeout, options);
    }

}
  • MyDataSource를 스프링 빈으로 등록하는 자바 설정이다.
  • Environment를 사용하면 외부 설정의 종류와 관계없이 코드 안에서 일관성 있게 외부 설정을 조회할 수 있다.
  • Environment.getProperty(key, Type)를 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다. (스프링 내부 변환기가 작동)
    • env.getProperty("my.datasource.etc.max-connection", Integer.class) : 문자 숫자로 변환
    • env.getProperty("my.datasource.etc.timeout", Duration.class) : 문자 Duration (기간) 변환
    • env.getProperty("my.datasource.etc.options", List.class) : List 변환 (A,B -> 문자 [A,B])

실행 결과


application.properties에 필요한 외부 설정을 추가하고, Environment를 통해서 해당 값들을 읽어서, MyDataSource를 만들었다. 향후 외부 설정 방식이 달라져도, 예를 들어서 설정 데이터(application.properties)를 사용하다가 커맨드 라인 옵션 인수나 자바 시스템 속성으로 변경해도 애플리케이션 코드를 그대로 유지할 수 있다.


하지만 이 방식의 단점은 Environment를 직접 주입받고, env.getProperty(key)를 통해서 값을 꺼내는 과정을 반복해야 한다는 점이다. 스프링은 @Value를 통해서 외부 설정값을 주입 받는 더욱 편리한 기능을 제공한다.


외부설정 사용 - @Value

@Value도 내부에서는 Environment를 사용해서 값을 채운다.


@Slf4j
@Configuration
public class MyDataSourceValueConfig {

    @Value("${my.datasource.url}")
    private String url;

    @Value("${my.datasource.username}")
    private String username;

    @Value("${my.datasource.password}")
    private String password;

    @Value("${my.datasource.etc.max-connection}")
    private int maxConnection;

    @Value("${my.datasource.etc.timeout}")
    private Duration timeout;

    @Value("${my.datasource.etc.options}")
    private List<String> options;

    @Bean
    public MyDataSource myDataSource1() {
        return new MyDataSource(url, username, password, maxConnection, timeout, options);
    }

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

}
  • @Value${}를 사용해서 외부 설정의 키 값을 주면 원하는 값을 주입 받을 수 있다.
  • @Value필드에 사용할 수도 있고, 파라미터에 사용할 수도 있다.
    • myDataSource1()은 필드에 주입 받은 설정값을 사용한다.
    • myDataSource2() 는 파라미터를 통해서 설정 값을 주입 받는다.

실행 결과


application.properties에 필요한 외부 설정을 추가하고, @Value를 통해서 해당 값들을 읽어서, MyDataSource를 만들었다.


@Value를 사용하는 방식도 좋지만, @Value로 하나하나 외부 설정 정보의 키 값을 입력받고, 주입 받아와야 하는 부분이 번거롭다. 그리고 설정 데이터를 보면 하나하나 분리되어 있는 것이 아니라 정보의 묶음으로 되어 있다. 여기서는 my.datasource 부분으로 묶여있다. 이런 부분을 객체로 변환해서 사용할 수 있다면 더 편리하고 더 좋을 것 같다.


외부설정 사용 - @ConfigurationProperties

Type-safe Configuration Properties

스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다. 이것을 타입 안전한 설정 속성이라 한다.

객체를 사용하면 타입을 사용할 수 있다. 따라서 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있고, 객체를 통해서 활용할 수 있는 부분들이 많아진다. 즉, 외부 설정을 자바 코드로 관리할 수 있는 것이다. 그리고 설정 정보 그 자체도 타입을 가지게 된다.


MyDataSourcePropertiesV1

@Setter
@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {

    private String url;

    private String username;

    private String password;

    private Etc etc;

    @Setter
    @Getter
    public static class Etc {

        private int maxConnection;

        private Duration timeout;

        private List<String> options = new ArrayList<>();
    }

}
  • 외부 설정을 주입 받을 객체를 생성한다. 그리고 각 필드를 외부 설정의 키 값에 맞추어 준비한다.
  • @ConfigurationProperties이 있으면 외부 설정을 주입 받는 객체라는 뜻이다. 여기에 외부 설정 KEY의 묶음 시작점인 my.datasource를 적어준다.
  • 기본 주입 방식은 자바빈 프로퍼티 방식이므로, Getter, Setter가 필요하다.

MyDataSourceConfigV1

@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
public class MyDataSourceConfigV1 {

    private final MyDataSourcePropertiesV1 properties;

    public MyDataSourceConfigV1(MyDataSourcePropertiesV1 properties) {
        this.properties = properties;
    }

    @Bean
    public MyDataSource myDataSource() {
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions()
        );
    }

}
  • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
    • 스프링에게 사용할 @ConfigurationProperties를 지정해주어야 한다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있다.
  • private final MyDataSourcePropertiesV1 properties 설정 속성을 생성자를 통해 주입 받아서 사용한다.

실행 결과


@ConfigurationPropertiesScan

  @ConfigurationPropertiesScan({ "com.hyuuny.app", "com.hyuuny.another" })
  @SpringBootApplication
  public class MyApplication {
    public static void main(String[] args) {
          SpringApplication.run(MyApplication.class, args);
      }
  }
  • @ConfigurationProperties를 하나하나 직접 등록할 때는 @EnableConfigurationProperties를 사용한다.
  • @ConfigurationProperties를 특정 범위로 자동 등록할 때는 @ConfigurationPropertiesScan을 사용하면 된다.
  • 빈을 직접 등록하는 것과 컴포넌트 스캔을 사용하는 차이와 비슷하다.

MyDataSourcePropertiesV1은 스프링 빈으로 등록된다. 그런데 Setter를 가지고 있기 때문에 누군가 실수로 값을 변경하는 문제가 발생할 수 있다. 여기에 있는 값들은 외부 설정값을 사용해서 초기에만 설정되고, 이후에는 변경하면 안된다. 이럴 때 Setter를 제거하고 대신에 생성자를 사용하면 중간에 데이터를 변경하는 실수를 근본적으로 방지할 수 있다.


대부분의 개발자가 MyDataSourcePropertiesV1의 값은 변경하면 안된다고 인지하고 있지만, 어떤 개발자가 자신의 문제를 해결하기 위해 setter를 통해서 값을 변경하게 되면, 애플리케이션 전체에 심각한 버그를 유발할 수 있다. 좋은 프로그램은 제약이 있는 프로그램이다.


외부설정 사용 - @ConfigurationProperties 생성자

@ConfigurationProperties는 Getter, Setter를 사용하는 자바빈 프로퍼티 방식이 아니라 생성자를 통해서 객체를 만드는 기능도 지원한다.


MyDataSourcePropertiesV2

@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {

    private String url;

    private String username;

    private String password;

    private Etc etc;

    public MyDataSourcePropertiesV2(String url, String username, String password, 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;
        }
    }

}
  • 생성자를 만들어 두면 생성자를 통해서 설정 정보를 주입한다.
  • @Getter: 롬복이 자동으로 getter 를 만들어준다.
  • @DefaultValue: 해당 값을 찾을 수 없는 경우 기본값을 사용한다.
    • @DefaultValue Etc etc
      • etc를 찾을 수 없을 경우 Etc 객체를 생성하고 내부에 들어가는 값은 비워둔다.(null, 0)
    • @DefaultValue("DEFAULT") List<String> options
      • options를 찾을 수 없을 경우 DEFAULT 라는 이름의 값을 사용한다.

실행 결과


application.properties에 필요한 외부 설정을 추가하고, @ConfigurationProperties의 생성자 주입을 통해서 값을 읽어들였다. Setter가 없으므로 개발자가 중간에 실수로 값을 변경하는 문제가 발생하지 않는다.


타입과 객체를 통해서 숫자에 문자가 들어오는 것 같은 기본적인 타입 문제들은 해결이 되었다. 그런데 타입은 맞는데 숫자의 범위가 기대하는 것과 다르면 어떻게 될까? 예를 들어서 max-conneciton의 값을 0 으로 설정하면 커넥션이 하나도 만들어지지 않는 심각한 문제가 발생한다고 가정해보자. max-conneciton은 최소 1 이상으로 설정하지 않으면 애플리케이션 로딩 시점에 예외를 발생시켜서 빠르게 문제를 인지할 수 있도록 하고 싶다.


외부설정 사용 - @ConfigurationProperties 검증

@ConfigurationProperties은 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원하고, 자바 빈 검증기를 사용하기 위해서는 spring-boot-starter-validation이 필요하다.

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

MyDataSourcePropertiesV3

@Getter
@Validated
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV3 {

    @NotEmpty
    private String url;

    @NotEmpty
    private String username;

    @NotEmpty
    private String password;

    private Etc etc;

    public MyDataSourcePropertiesV3(String url, String username, String password, 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초를 허용한다.

MyDataSourceConfigV3

@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV3.class)
public class MyDataSourceConfigV3 {

    private final MyDataSourcePropertiesV3 properties;

    public MyDataSourceConfigV3(MyDataSourcePropertiesV3 properties) {
        this.properties = properties;
    }

    @Bean
    public MyDataSource myDataSource() {
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions()
        );
    }

}

my.datasource.url=local.db.com
my.datasource.username=hyuuny
my.datasource.password=password
my.datasource.etc.max-connection=0
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN
  • my.datasource.etc.max-connection의 값을 0으로 수정한다.

실행 결과


ConfigurationProperties 덕분에 타입 안전하고, 또 매우 편리하게 외부 설정을 사용할 수 있다. 그리고 검증기 덕분에 쉽고 편리하게 설정 정보를 검증할 수 있다. 가장 좋은 예외는 컴파일 예외, 그리고 애플리케이션 로딩 시점에 발생하는 예외이다. 가장 나쁜 예외는 사용자가 사용 중에 발생하는 런타임 예외이다.


ConfigurationProperties 장점

  • 외부 설정을 객체로 편리하게 변환해서 사용할 수 있다.
  • 외부 설정의 계층을 객체로 편리하게 표현할 수 있다.
  • 외부 설정을 타입 안전하게 사용할 수 있다.
  • 검증기를 적용할 수 있다.



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

'Spring' 카테고리의 다른 글

[Spring] actuator (액츄에이터)  (0) 2023.04.10
[Spring] YAML , @Profile  (0) 2023.04.09
[Spring] 외부설정과 프로필 2  (0) 2023.04.03
[Spring] 외부설정과 프로필 1  (0) 2023.04.02
[Spring] build.gradle 라이브러리 관리  (0) 2023.03.13
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
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
글 보관함