ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot 핵심 기능 4 - 외부설정(2)
    BackEnd/Spring Boot 2023. 5. 4. 07:00
    반응형

      이전 포스팅에서 다루었던 커맨드 라인 옵션 인수, 자바 시스템 속성, OS 환경변수는 모두 외부 설정을 key=value 형식으로 사용할 수 있는 방법입니다. 그런데 어디에 있는 외부 설정값을 읽어야 하는지에 따라 각각 읽는 방법이 다르다는 단점이 있습니다. 스프링은 이 문제를 EnvironmentPropertySource라는 추상화를 통해서 해결합니다.

     

    스프링의 외부 설정 통합

      스프링은 로딩 시점에 필요한 PropertySource들을 생성하고, Environment에서 사용할 수 있게 연결합니다. 모든 외부 설정은 environment.getProperty(key)를 통해서 조회하면 됩니다. application.properties, application.yml도 PropertySource에 추가되기에 Environment를 통해서 접근할 수 있습니다.

     

    package hello;
    
    import jakarta.annotation.PostConstruct;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.env.Environment;
    import org.springframework.stereotype.Component;
    
    @Slf4j
    @Component
    public class EnvironmentCheck {
    
        private final Environment env;
    
        public EnvironmentCheck(Environment env) {
            this.env = env;
        }
    
        @PostConstruct
        public void init() {
            String url = env.getProperty("url");
            String username = env.getProperty("username");
            String password = env.getProperty("password");
            log.info("env url={}", url);
            log.info("env username url={}", username);
            log.info("env password url={}", password);
        }
    }
    • 커맨드 라인 옵션 인수 실행: --url=devdb --username=dev_user --password=dev_pw
    • 자바 시스템 속성 실행: -Durl=devdb -Dusername=dev_user -Dpassword=dev_pw

     

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

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

     

      다음의 예제 코드를 기반으로 외부 설정을 읽어서 활용하는 다양한 방법들을 보겠습니다.

    MyDataSource.class

    package hello.datasource;
    
    import jakarta.annotation.PostConstruct;
    import lombok.extern.slf4j.Slf4j;
    
    import java.time.Duration;
    import java.util.List;
    
    @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);
        }
    }

    application.properties

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

     

    Environment

    package hello.config;
    
    import hello.datasource.MyDataSource;
    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 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.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 변환

     

    @Value

      @Value를 사용하면 외부 설정값을 편리하게 주입받을 수 있습니다. @Value도 내부에서는 Environment를 사용합니다.

    package hello.config;
    
    import hello.datasource.MyDataSource;
    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 aMyDataSourceValueConfig {
    
        @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)할 수도 있습니다.
    • 기본값을 사용하려면 다음과 같이 : 뒤에 기본값을 적어주면 됩니다.
      예) @Value("${my.datasource.etc.max-connection:1}"): key가 없는 경우 1을 사용합니다.

     

    @ConfigurationProperties

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

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

     

    package hello.datasource;
    
    import lombok.Data;
    import lombok.Getter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.context.properties.bind.ConstructorBinding;
    import org.springframework.boot.context.properties.bind.DefaultValue;
    
    import java.time.Duration;
    import java.util.ArrayList;
    import java.util.List;
    
    @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, @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;
            }
        }
    
    }
    • @ConfigurationProperties이 있으면 외부 설정을 주입 받는 객체라는 뜻입니다. 여기에 외부 설정 KEY의 묶음 시작점인 my.datasource를 적어줍니다.
    • @DefaultValue: 해당 값을 찾을 수 없는 경우 기본값을 사용합니다.

     

    Note. @ConstructorBinding

    스프링 3.0 이전에는 생성자 바인딩 시에 @ConstructorBinding 애노테이션을 필수로 사용해야 했습니다.

    스프링 3.0 부터는 생성자가 하나일 때는 생략할 수 있으며, 둘 이상인 경우 사용할 생성자에 @ConstructorBinding 애노테이션을 적용하면 됩니다.

     

    package hello.config;
    
    import hello.datasource.MyDataSource;
    import hello.datasource.MyDataSourcePropertiesV2;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    
    @Slf4j
    @EnableConfigurationProperties(MyDataSourcePropertiesV2.class)
    public class MyDataSourceConfigV2 {
    
        private final MyDataSourcePropertiesV2 properties;
    
        public MyDataSourceConfigV2(MyDataSourcePropertiesV2 properties) {
            this.properties = properties;
        }
    
        @Bean
        public MyDataSource dataSource() {
            return new MyDataSource(
                    properties.getUrl(),
                    properties.getUsername(),
                    properties.getPassword(),
                    properties.getEtc().getMaxConnection(),
                    properties.getEtc().getTimeout(),
                    properties.getEtc().getOptions());
        }
    }
    • @EnableConfigurationProperties(MyDataSourcePropertiesV2.class): 스프링에게 사용할 @ConfigurationProperties를 지정해주어야 합니다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있습니다.
    • private final MyDataSourcePropertiesV2 properties: 설정 속성을 생성자를 통해 주입 받아서 사용합니다.

     

    @ConfigurationProperties 검증

      자바 빈 검증기를 사용하려면 spring-boot-starter-validation이 필요합니다. 다음과 같이 의존성을 추가합니다.

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

     

      검증기를 추가해서 ConfigurationProperties을 만듭니다.

    package hello.datasource;
    
    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.datasource")
    @Validated
    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, @DefaultValue("DEFAULT") List<String> options) {
                this.maxConnection = maxConnection;
                this.timeout = timeout;
                this.options = options;
            }
        }
    
    }

     

    YAML

      스프링은 설정 데이터를 사용할 때 application.properties 뿐만 아니라 application.yml 이라는 형식도 지원합니다. YAML(YAML Ain't Markup Language)은 사람이 읽기 좋은 데이터 구조를 목표로 하며, 확장자는 yaml, yml 입니다. 주로 yml을 사용합니다.

     

    application.yml

    my:
      datasource:
        url: local.db.com
        username: local_user
        password: local_pw
        etc:
          max-connection: 1
          timeout: 60s
          options: LOCAL, CACHE
    • YAML의 가장 큰 특징은 사람이 읽기 좋게 계층 구조를 이룬다는 점입니다.
    • YAML은 space(공백)로 계층 구조를 만듭니다. space는 1칸을 사용해도 되는데, 보통 2칸을 사용합니다.
    • 구분 기호로 : 를 사용합니다.

     

    주의. application.properties, application.yml을 같이 사용하면 application.properties가 우선권을 가집니다. 이것을 둘이 함께 사용하는 것은 일관성이 없으므로 권장하지 않으며, 실무에서는 설정 정보가 많아서 보기 편한 yml을 선호합니다.

     

      다음은 yml에 프로필을 적용한 예입니다.

    my:
      datasource:
        url: local.db.com
        username: local_user
        password: local_pw
        etc:
          max-connection: 1
          timeout: 60s
          options: LOCAL, CACHE
    ---
    spring:
      config:
        activate:
          on-profile: dev
    my:
      datasource:
        url: dev.db.com
        username: dev_user
        password: dev_pw
        etc:
          max-connection: 10
          timeout: 60s
          options: DEV, CACHE
    ---
    spring:
      config:
        activate:
          on-profile: prod
    my:
      datasource:
        url: prod.db.com
        username: prod_user
        password: prod_pw
        etc:
          max-connection: 50
          timeout: 10s
          options: PROD, CACHE

     

    @Profile

      @Profile은 특정 조건에 따라서 해당 빈을 등록할지 말지 선택합니다. 코드를 보면 @Conditional(ProfileCondition.class)를 확인할 수 있습니다. 스프링은 @Conditional 기능을 활용해서 개발자가 더 편리하게 사용할 수 있는 @Profile 기능을 제공하는 것입니다. @Profile을 사용하면 각 환경 별로 외부 설정 값을 분리하는 것을 넘어 등록되는 스프링 빈도 분리할 수 있습니다.

    package org.springframework.context.annotation;
    ...
    @Conditional(ProfileCondition.class)
    public @interface Profile {
      String[] value();
    }

     

      다음은 default 프로필(기본값)이 활성화 되어 있으면 LocalPayClient를, prod 프로필이 활성화 되어 있으면 ProdPayClient를 빈으로 등록하는 예제 코드입니다.

    package hello.pay;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    
    @Slf4j
    @Configuration
    public class PayConfig {
    
        @Bean
        @Profile("default")
        public LocalPayClient localPayClient() {
            log.info("LocalPayClient 빈 등록");
            return new LocalPayClient();
        }
    
        @Bean
        @Profile("prod")
        public ProdPayClient prodPayClient() {
            log.info("ProdPayClient 빈 등록");
            return new ProdPayClient();
        }
    }

     

    [참고정보]

    스프링 부트 - 핵심 원리와 활용

    반응형

    댓글

Designed by Tistory.