본문 바로가기
Devops

[Spring Boot] 다양한 외부 설정 방법 #3 - @Value, @ConfigurationProperties

by 개미가되고싶은사람 2024. 10. 15.

목차

    @Value

    application.properties 파일에 필요한 외부 설정을 추가한 후, @Value 애노테이션을 통해 해당 값들을 손쉽게 읽어올 수 있습니다. @Value는 내부적으로 Environment를 사용하여 설정 값을 가져옵니다. 이 애노테이션은 필드에 적용할 수 있을 뿐만 아니라, 메서드의 파라미터에도 사용할 수 있어 유연한 설정할 수 있습니다.

    만약 특정 키를 찾지 못할 경우, 코드에서 기본값을 사용하고 싶다면, @Value 애노테이션의 값 뒤에 콜론(:)을 추가하고 기본값을 적어주면 됩니다. 

    @Value("${my.datasource.etc.max-connection:1}")

     

    추가로, @Value를 사용할 때는 설정 값의 타입에 맞게 변환이 이루어지므로, 적절한 타입으로 변환될 수 있도록 주의해야 합니다.

     

    파라미터 적용 예시

       public MyDataSource myDataSource(
                @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)

     

    필드 적용 예시

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

     

    @Value의 단점

    @Value를 사용하여 소수의 외부 설정 정보를 키 값으로 입력받고 주입받는 것은 간편하지만, 많은 외부 설정 값을 처리하는 과정은 다소 번거로울 수 있습니다. 특히 설정 데이터를 살펴보면, 개별적으로 분리되어 있는 것이 아니라 정보의 묶음으로 제공되는 경우가 많습니다.

    예를 들어, my.datasource와 같이 관련된 설정들이 함께 묶여 있는 경우가 많습니다. 이러한 설정들을 하나하나 @Value로 주입받는 것은 비효율적이며, 코드의 가독성을 떨어뜨릴 수 있습니다.

    이런 경우에는 설정 데이터를 객체로 변환하여 사용하는 것이 더 편리하고 효율적입니다. 이렇게 하면 관련된 설정 값을 하나의 객체로 묶어 관리할 수 있어, 많은 외부 설정 값을 다룰 때 더욱 유용합니다.

     

     

    @ConfigurationProperties

    스프링 부트는 외부 설정 정보를 객체로 변환하는 기능을 제공하여 애플리케이션 설정을 보다 쉽게 관리할 수 있도록 합니다. 이 기능을 '타입 안전한 설정 속성'이라고 하며, @ConfigurationProperties를 사용하면 설정 값을 그룹화하여 체계적으로 관리할 수 있습니다.

    이러한 방식은 복잡한 설정을 보다 명확하게 정리할 수 있게 해주며, 특정 설정 값이 예를 들어 int 자료형이어야 할 경우, 잘못된 타입(예: 문자열)이 들어오면 스프링이 오류 메시지를 반환하여 개발자가 문제를 쉽게 인지하고 수정할 수 있도록 도와줍니다.

    Failed to bind properties under 'my.datasource.etc.max-connection' to int:
    
        Property: my.datasource.etc.max-connection
        Value: "test"
        Origin: class path resource [application.properties] - 4:34
        Reason: failed to convert java.lang.String to int (caused by java.lang.NumberFormatException: For input string: "test")

    또한, @ConfigurationProperties를 사용할 때는 getter와 setter 메서드가 필요합니다. 이는 스프링이 해당 프로퍼티 값을 자동으로 바인딩하고, 스프링 빈으로 등록하여 필요한 곳에서 쉽게 사용할 수 있도록 하기 위함입니다.

     

    @ConfigurationProperties 데이터 검증

    @ConfigurationProperties를 사용하면 외부 설정에서 가져온 값의 타입을 안전하게 관리할 수 있습니다. 그러나 숫자의 범위나 문자열의 길이와 같은 추가적인 검증은 기본적으로 제공되지 않기 때문에 개발자가 직접 검증 로직을 작성해야 할 필요가 있습니다. 이를 해결하기 위해 자바에서는 자바 빈 검증기(Java Bean Validation)라는 강력한 표준 검증기를 제공합니다. 

     

    외부 설정 프로퍼티(객체)

    @Getter
    @Setter
    @ConfigurationProperties("my.datasource") 
    @Validated
    public class MyDataSourceProperties {
    
        @NotEmpty
        private String url;
        @NotEmpty
        private String username;
        @NotEmpty
        private String password;
        private Etc etc;
    
        @Getter
        @Setter
        public static class Etc {
            @Min(1)
            @Max(99)
            private int maxConnection;
    
            @DurationMin(seconds =1)
            @DurationMax(seconds =60)
            private Duration timeout;
            private List<String> options = new ArrayList<>();
    
        }
    
    }

    @Validated를 클래스에 추가하면, 스프링이 해당 클래스의 필드에 적용된 검증 애노테이션(예: @NotEmpty, @Min, @Max, @DurationMin, @DurationMax)을 자동으로 검사합니다. 이를 통해 외부 설정 값이 유효한지 확인할 수 있습니다.

     

     

    설정

    @Slf4j
    @EnableConfigurationProperties(MyDataSourceProperties.class)
    public class MyDataSourceConfig {
    
        private final MyDataSourceProperties properties;
    
        public MyDataSourceConfig(MyDataSourceProperties 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(MyDataSourceProperties.class)는 스프링에서 특정 프로퍼티 클래스를 활성화하여 해당 클래스의 프로퍼티를 애플리케이션의 설정 파일(예: application.properties 또는 application.yml)에서 읽어올 수 있도록 하는 애노테이션입니다.

     

    추가적으로 @ConfigurationPropertiesScan과 @EnableConfigurationProperties는 스프링에서 설정 프로퍼티를 관리하는 애노테이션으로, @EnableConfigurationProperties는 특정 클래스에 대해 @ConfigurationProperties를 직접 등록할 때 사용되며, 예를 들어 @EnableConfigurationProperties(MyDataSourceProperties.class)와 같이 특정 프로퍼티 클래스를 명시적으로 등록합니다. 반면, @ConfigurationPropertiesScan은 특정 범위 내의 모든 @ConfigurationProperties 클래스를 자동으로 등록할 때 사용되어 여러 프로퍼티 클래스를 한 번에 스캔하고 등록할 수 있습니다. 따라서 개별적으로 프로퍼티 클래스를 등록하고 싶다면 @EnableConfigurationProperties를, 여러 클래스를 자동으로 등록하고 싶다면 @ConfigurationPropertiesScan을 사용하는 것이 좋습니다.

    @SpringBootApplication
    @ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
    public class MyApplication {}

     

     

    @ConfigurationProperties의 문제: Setter 제거의 필요성

    MyDataSourceProperties는 스프링 빈으로 등록되며, 외부 설정값을 사용하여 초기화됩니다. 그러나 Setter 메서드가 존재하면 실수로 값이 변경될 가능성이 있습니다. 이러한 변경은 예기치 않은 버그를 초래할 수 있습니다.

    따라서 Setter를 제거하고 생성자를 통해 값을 설정하는 방식으로 변경하면, 중간에 데이터가 수정되는 것을 방지할 수 있습니다. 생성자를 사용하면 객체가 생성될 때 필요한 모든 값을 한 번에 설정할 수 있어, 이후에 값이 변경되지 않도록 보장할 수 있습니다. 비록 대부분의 개발자가 MyDataSourceProperties의 값은 변경해서는 안 된다는 것을 인지하고 있지만, 어떤 개발자가 자신의 문제를 해결하기 위해 Setter를 사용하게 되면 애플리케이션 전체에 심각한 영향을 미칠 수 있습니다.

    @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(99)
            private int maxConnection;
    
            @DurationMin(seconds =1)
            @DurationMax(seconds =60)
            private Duration timeout;
            private List<String> options = new ArrayList<>();
    
            public Etc(int maxConnection, Duration timeout, List<String> options) {
                this.maxConnection = maxConnection;
                this.timeout = timeout;
                this.options = options;
            }
        }
    
    }