본문 바로가기
Lecture/김영한 - 스프링 부트 - 핵심 원리와 활용

스프링 부트 - 핵심 원리와 활용 - 외부설정과 프로필2

by Soono991 2023. 3. 12.

💡이 포스팅은 김영한 님의 인프런 강의인 스프링 부트 - 핵심 원리와 활용 수강하고 학습한 내용을 정리한 포스팅입니다.

 

김영한 님의 강의를 수강하며 정리한 GitHub Repository입니다.

 

GitHub - kiekk/inflearn-kyh-spring-boot

Contribute to kiekk/inflearn-kyh-spring-boot development by creating an account on GitHub.

github.com

 

이번 챕터에 대해 정리할 내용은 다음과 같습니다.

  • Environment
  • @Value
  • @ConfigurationProperties
  • spring-configuration-metadata.json

 

이전 포스팅에서도 정리했던 것처럼 Spring에서는 다양한 외부 설정 정보를 읽어올 수 있도록 Environment라는 추상화된 객체를 제공하고 있습니다.

 

Spring은 여기서 한 걸음 더 나아가 Environment를 활용해 더 편리하게 읽어올 수 있도록 @Value, @ConfigurationProperties를 제공하고 있습니다.

 

Environment

가장 기본적인 방법으로 Environment 객체를 주입받아 설정 정보를 읽어오는 방법입니다.

public class EnvironmentCheck {

    // Environment 를 사용하여 외부 설정을 읽는 방법을 통일
    private final Environment 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 = {}", username);
        log.info("env password = {}", password);
    }
}

getProperty() 메서드를 통해 설정 정보의 값을 읽어올 수 있는데, 위와 같이 name만 작성하게 되면 그에 해당하는 값이 String 형태로 반환됩니다. 만약 name에 해당하는 값이 없을 경우 null이 반환되는데, 오버로딩 된 메서드들을 통해 기본 값을 설정하거나 특정 타입의 값으로 반환받을 수 있습니다.

@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);
    }
}

 

@Value

두 번째 방법은 @Value annotation을 사용하는 방법입니다. 

@Value annotation은 필드 또는 메서드나 생성자의 파라미터 레벨에서 사용할 수 있으며, #{systemProperties.myProp}과 같은 형식이거나 SpEL 형식으로 사용해야 합니다.

 

#{systemProperties.myProp:defaultValue}

#: systemProperties에 접근할 때는 #를 사용합니다.
$: 우리가 작성한 custom properties에 접근할 때는 $를 사용합니다.

@Value annotation의 value에 작성한 표현식에 대한 설정 정보가 없을 경우 에러가 발생하는데 이를 방지하기 위해 

: 콜론을 사용해 기본 값을 설정할 수도 있습니다.

위와 같은 표현식으로 작성했을 경우 값을 파싱 하는 객체는 PropertyPlaceHolderConfigurer라는 객체입니다.

이 객체에 대해서는 토비님의 인프런 강의 포스팅에서 정리했으니 참고하시면 좋을 것 같습니다.

 

공식 문서에서 언급된 것처럼 @Value annotationa을 사용하면 Single Application Arguments를 주입할 수 있다고 하는데, 이 말은 즉 property와 field들이 1:1로 바인딩되며 여러 개의 property를 Object로 바인딩할 수 없다는 뜻입니다.

이렇게 여러 개의 property를 Object로 바인딩하고 싶은 경우에는 @Value 대신 @ConfigurationProperties를 사용하면 됩니다.

 

 

토비의 스프링 부트 - 외부 설정을 이용한 자동 구성

💡이 포스팅은 토비님의 인프런 강의인 토비의 스프링 부트 - 이해와 원리를 수강하고 학습한 내용을 정리한 포스팅입니다. 토비님의 강의를 수강하며 정리한 GitHub Repository입니다. GitHub - kiekk/i

soono-991.tistory.com

 

@ConfigurationProperties

공식 문서에서도 언급된 것처럼 @Value annotation을 사용하게 되면 필드와 파라미터에 사용해야 하기 때문에 일일이 필드나 파라미터에 annotation을 작성해서 설정 정보를 매핑해야 하는 단점이 있습니다.

그리고 설정 정보의 경우 논리적으로 하나의 묶음으로 관리될 수 있는데 @Value를 사용할 경우 이를 각각 매핑하기 때문에 관련 있는 설정 정보를 함께 묶어 관리할 수 없습니다.

 

이런 단점을 @ConfigurationProperties annotation이 해결해 주며, @ConfigurationProperties annotation을 사용하게 되면 설정 정보를 객체 단위로 매핑할 수 있게 됩니다.

객체 단위로 매핑하게 되면 설정 정보의 값이 없어도 에러가 발생하지 않으며, 관련 있는 설정 정보를 객체 단위로 묶어 관리할 수 있게 됩니다.

 

// 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

위와 같이 설정 정보가 있다고 하면, 이를 객체로 매핑하기 위해서는 다음과 같이 코드를 작성하면 됩니다.

@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {
    private String url;
    private String username;
    private String password;
    private Etc etc = new Etc();

    @Data
    public static class Etc {
        private int maxConnection;
        private Duration timeout;
        private List<String> options = new ArrayList<>();
    }
}
@ConfigurationProperties("my.datasource")

→ "my.datasource"는 prefix로써 설정 정보가 해당 값으로 시작하는 설정 정보를 찾습니다.
→ 그 설정 정보들 중에 작성한 필드 이름과 일치하는 설정 정보의 값을 매핑하게 됩니다.
→이때 @ConfigurationProperties 설명을 살펴보면 기본적으로 setter를 호출해서 바인딩을 하는데, @ConstructorBinding을 사용하게 되면 생성자를 통해 바인딩을 한다고 합니다.

하지만 @ConstructorBinding은 3.0부터 Deprecated 되었으며, 생성자가 1개일 경우에는 생략이 가능하다고 합니다.

Annotation that can be used to indicate which constructor to use when binding configuration properties using constructor arguments rather than by calling setters. A single parameterized constructor implicitly indicates that constructor binding should be used unless the constructor is annotated with `@Autowired`.

setter를 호출하는 대신 생성자 인수를 사용하여 구성 속성을 바인딩할 때 사용할 생성자를 나타내는 데 사용할 수 있는 주석입니다. 매개변수화된 단일 생성자는 생성자에 `@Autowired` 주석이 지정되지 않은 경우 생성자 바인딩을 사용해야 함을 암시적으로 나타냅니다.

💡Spring Bean 주입 시 생성자를 한 개만 사용할 경우 @Autowired를 생략해도 되는 이유가 바로 여기에 있습니다.

 

이번엔 생성자를 통해 설정 정보를 바인딩해보겠습니다.

@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;
        }
    }
}

이번엔 생성자를 사용할 것이기 때문에 Lombok에 Setter는 추가하지 않았습니다.

값을 확인해 보면 정상적으로 바인딩된 것을 확인할 수 있습니다.

여기서 @DefaultValue annotation을 사용하면 설정 정보의 값이 없을 경우 기본값을 설정할 수 있습니다.

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 #주석
@Getter
public static class Etc {
    private int maxConnection;
    private Duration timeout;
    private List<String> options;

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

 

@Value vs @ConfigurationProperties

위에서 설명했던 @Value와 @ConfigurationProperties의 차이점에 대해 정리해 보면 아래와 같습니다.

공식 문서 참고

 

여기서 Relaxed binding이란 property 이름의 형식을 다양한 조합을 통해 찾아 바인딩하는 것을 말하는데, 아래와 같은 형식으로 property 이름을 찾는다고 합니다. 하지만 @Value의 경우 제한적이라고 하며 Note에서 처럼 케밥 케이스가 아닌 캐멀 케이스로 사용할 경우 다른 형식의 property를 찾지 않는다고 합니다.

💡Relaxed Binding의 Note에서 언급된 것처럼 prefix 값은 반드시 케밥케이스가 있어야 한다고 합니다.
케밥케이스가 없을 경우 에러가 발생합니다.

 

Meta-data Support는 해당 property의 메타 정보를 지원하냐는 것인데, @Value를 사용할 경우 아래 설명할 spring-configuration-metadata.json 파일에 메타 정보가 추가되지 않습니다.

 

반면 @Value에서는 SpEL문법을 사용할 수 있지만 @ConfigurationProperties는 SpEL을 사용할 수 없습니다.

 

spring-configuration-metadata.json

annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

build.gradle에 configuration-processor 의존성을 추가하게 되면 build 시 @ConfigurationProperties가 붙은 클래스에 대한 Configuration Metadata File인 spring-configuration-metadata.json 파일을 생성해 줍니다.

 

spring-configuration-metadata.json 파일 내부에 group, properties, hints 항목들이 존재하는 것을 확인할 수 있습니다.

hints는 현재 [] 빈 배열로 되어있는데, hints에는 각 항목들에 사용할 수 있는 값들을 미리 작성해서 개발자들에게 가이드를 해줄 수 있습니다.

 

hints를 추가하기 위해서는 resources/META-INF/additional-spring-configuration-metadata.json 파일을 추가로 작성하면 됩니다. (공식문서 참고)

@ConfigurationProperties에 바인딩되지 않는 속성에 대한 정보를 추가하거나, 기존의 속성 정보를 변경하고 싶은 경우에 additional-spring-configuration-metadata.json 파일을 사용한다고 합니다.

 

파일을 추가한 후 아래와 같이 hints를 작성합니다.

 

{
  "hints": [
    {
      "name": "my.datasource.username",
      "values": [
        {
          "value": "local_user"
        },
        {
          "value": "dev_user"
        },
        {
          "value": "prod_user"
        }
      ]
    },
    {
      "name": "my.datasource.password",
      "values": [
        {
          "value": "local_pw"
        },
        {
          "value": "dev_pw"
        },
        {
          "value": "prod_pw"
        }
      ]
    }
  ]
}

이렇게 작성한 후 다시 build를 해보면 이번엔 hints 항목이 추가되어 있는 것을 확인할 수 있습니다.

 

이제 jar 파일을 다른 프로젝트에 추가한 후 configuration-metadata.json 파일이 정상적으로 동작하는지 확인해 보겠습니다.

프로퍼티 자동 완성과 입력 값 가이드도 정상적으로 동작하는 것을 확인할 수 있습니다.

 

더 다양한 옵션과 상세한 내용은 아래 공식 문서를 참고하시면 좋을 것 같습니다.

 

 

Configuration Metadata

Configuration metadata files are located inside jars under META-INF/spring-configuration-metadata.json. They use a JSON format with items categorized under either “groups” or “properties” and additional values hints categorized under "hints", as sh

docs.spring.io

 

 

@Value, @ConfigurationProperties에 대해 이전에 정리했던 포스팅이 있어 같이 참고하면 좋을 것 같습니다.

 

 

property 값 읽기

개발에 필요한 정보들을 yml, properties 등에 작성하게 되는데, 오늘은 property 파일에 작성한 값들을 읽...

blog.naver.com

 

property 값 읽기 - 2

이전 포스팅에서 @Value, @ConfigurationProperties 어노테이션을 사용해서 property 파일의 값을 읽어...

blog.naver.com

댓글