본문 바로가기
백엔드/스프링

@Value vs @ConfigurationProperties

by ARlegro 2025. 4. 13.

기존에 필드값 or 파라미터에 환경변수값을 동적으로 주입시킬 때 @Value를 써왔다.

하지만 더 좋은 대안인 @ConfigurationProperties가 있다는 걸 알게된 후 그것에 대해 알아보기로 했다.

목차

  • @Value 간단 소개
  • @Value 단점
  • @ConfigurationProperties 소개

@Value 소개

✅ 개념

스프링에서 컨테이너가 관리하는! 객체의 속성이나 파라미터 등에 외부값을 주입할 때 사용된다.

  • 외부 설정값을 손쉽게 주입 가능

예시

public S3BinaryContentStorage(@Value("${discodeit.storage.s3.access-key}") String accessKey,

✅ 사용방법

A common use case is to inject values using #{systemProperties.myProp} style SpEL (Spring Expression Language) expressions. Alternatively, values may be injected using ${my.app.myProp} style property placeholders.

사용방법은 두 가지 방식으로 외부값을 주입할 수 있다.

1️⃣ #{} 형식 - SqEL 형식의 동적 바인딩

SqEL(스프링 표현 언어)를 사용해 동적으로 값을 주입

  • 직접 시스템 프로퍼티, 환경변수 참조가 가능
  • 이 외에도, 단순 값을 추출 및 계산을 넘어서, 메서드를 호출, 조건문 등 복잡한 표현도 가능하다는 장점이 있다.

예시

@Value("#{2 + 3}")
private int number;  // 결과: 5

@Value("#{systemProperties['my.prop']}")  // System.getProperty("my.prop")
private String sysProp;

💢 #{} 형식 단점

근데 이런 외부값 접근 방식은 스프링의 Environment 추상화 효과를 얻지 못하는거라 실제로는 2번 ${}를 많이쓴다.

 

2️⃣ ${} 형식 - 정적 바인딩(Propery Placeholder 방식) ⭐⭐

스프링의 Environment 추상화 구조를 활용한 방식

내부적으로 Environment가 해석하여 처리

  • 가장 일반적인 방식
  • 내부적으로 Environment.getProperty() 호출
  • @Value("${my.prop}") ==> 스프링의 해석 ==> Environment.getProperty("my.prop")
  • yml, OS 환경변수, 자바 시스템 프로퍼티 등 다양한 소스를 일관된 방식으로 접근
    • 이로 인해, 테스트 시 유리
    • 참고로, 등록된 ProperySource 우선순위에 따라 값을 해석

☑️ 추가 : 혼합해서도 사용 가능

@Value("#{${some.number} + 10}")  // 먼저 ${}로 값 가져오고, 그걸로 연산

하지만, 가독성이 너무 떨어져서 최대한 분리할 수 있으면 분리하는 것이 좋은 것 같다.

✅ 어디에 쓸 수 있는가?

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Value.html

@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
public @interface Value
  1. 필드값
    • 가장 흔하게 쓰는 방식이다.
    @Value("${discodeit.storage.s3.presigned-url-expiration}")
    private final Long presinged_url_expiration;
    
  2. 메서드/생성자 파라미터 값
        public Storage(@Value("${discodeit.storage.s3.access-key}") String accessKey,
                      @Value("${discodeit.storage.s3.secret-key}") String secretKey,
                      @Value("${discodeit.storage.s3.region}") String region,
                      @Value("${discodeit.storage.s3.bucket}") String bucket,
                      @Value("${discodeit.storage.s3.presigned-url-expiration}") Long presinged_url_expiration) 
    ​
  3. 애노테이션 내부

 

✅ 작동 원리 - 언제, 어떻게 처리

@Value는 스프링 컨테이너가 빈(bean)을 생성하고 초기화하는 과정에서 처리된다.

 

@Value가 어떻게 쓰였냐에 따라 처리 시점이 달라진다

  • 생성자 파라미터일 경우 → 빈 생성 전 주입 by ConstructorResolver
  • 필드, 세터 등 → 빈 생성 후 주입 by 빈 후처리기(Post Processor)

@Value 단점

1️⃣ 하드 코딩

  • 오타, 키 변경 시 일일이 찾아야 됨
  • 스펠링 한 글자만 틀려도 null 주입

2️⃣ 타입 안정성 부족 ‼️

  • 컴파일 시점에 타입체크가 되지 않음 Cuz 문자열 기반 바인딩
  • 즉, 잘못된 타입을 주입 시 런타임에 오류가 난다.
  • 예를 들어, 아래와 같이 필드값 주입을 사용했을 때, 컵파일 시점에는 오류가 안 나타난다.하지만, 애플리케이션 실행시 다음과 같은 타입 오류가 남
  • Error creating bean with name 'hello.config.MyAwsConfig': Unsatisfied dependency expressed through field 'ex': Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; For input string: "access-key1"
  • public class MyAwsConfig { @Value("${discodeit.storage.s3.access-key}") private Long ex;

3️⃣ 중복된 코드

  • 하나의 설정에 여러 프로퍼티를 사용할 경우 반복되는 코드가 많아진다.
  • 설정이 점점 더 많아지면 생성자, 필드가 너무 더러워진다.
@Value("${s3.key}") private String key;
@Value("${s3.secret}") private String secret;
@Value("${s3.bucket}") private String bucket;
@Value("${s3.region}") private String region;

4️⃣ @Validated 유효성 검사 불가

  • @Value는 자동 유효성 검사를 지원하지 않는다.

5️⃣ 그 외, 테스트 어려움 + 복잡한 구조(List, Map) 등을 바인딩하기 어려움

 

@ConfigurationPropreties 시작

✅ 개념

외부 설정 값을 객체 단위로 바인딩 할 때 사용하는 애노테이션

application.yml, OS 환경 변수, 자바 시스템 프로퍼티 등 외부에 정의된 설정값들을 하나의 Java 클래스에 자동으로 매핑해준다. 특징

  • 객체 단위 바인딩 → 여러 설정을 하나의 클래스에 통합해서 사용
  • 계층적 바인딩 가능
  • 세터 or 생성자 기반 바인딩 가능
  • 유효성 검사 가능
  • SpEL은 사용 불가

 

✅ 장점

1️⃣ 유효성 검사 가능

  • 자바 빈 검증기를 활용해 값을 검증할 수 있따.
  • @Validated를 사용하면 바인딩 시점에 자동 검증
@ConfigurationProperties("my.datasource")
@Validated
public class MyDataSourcePropertiesV3 {

 @NotEmpty
 private String url;
 
 @Min(1)
 @Max(999)
 private int maxConnection;

 

2️⃣ 타입 안정성

  • 속성이 객체 필드에 맞춰 자동변환 → 복잡한 List, Map 사용시 유용
my.datasource:
  urls:
    - jdbc:mysql://localhost
    - jdbc:mysql://replica

private List<String> urls;
  • 속성 검증
    • 아래의 의존성을 추가하면,
      • 런타임 전에 속성 검증을 해준다.
      • prefix설정에 따른 yml 자동완성 기능을 제공한다.
dependencies {
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}

사진을 보면 바인딩하는 객체의 생성자의 속성타입과 YML 파일에 적은 설정값의 타입이 불안정하면 빨간 오류가 뜬다.

 

 

3️⃣ 중복 제거 (prefix 를 통해)

  • 같은 접두어(prefix)를 가진 설정값들을 하나의 클래스에 묶어서 관리할 수 있어,@Value("${...}") 방식처럼 일일이 지정할 필요 없음
  • prefix 방법 : 객체 바인딩 될 수 있는 프로퍼티 접두어(prefix)를 지정한다
  • @ConfigurationProperties(prefix = "storage.s3") public static class MyAwsPropertiesV2 { private final String accessKey; [yml] storage: s3: access-key: access-key1

4️⃣ 관심사 분리 - SRP 원칙

  • 설정 전용 클래스를 따로 만들어, 로직과 설정 코드를 분리 → 유지보수성 Good
  • @Value를 사용할 때는 메인 로직의 파일에 덕지덕지 사용했지만 @ConfigurationProperties방법은 따로 파일을 두어 빈으로 등록하는 방법

✅ 관련 애노테이션

  1. @ConfiguratoinPropertiesScan : 자동 스캔을 지원한다.
    [애플리케이션 실행 메서드에 붙이면됨]
    
    @ConfigurationPropertiesScan
    public class ExternalReadApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ExternalReadApplication.class, args);
        }
    ​
  2. @EnableConfigurationProperties
    • 특정 클래스를 명식적으로 설정 클래스에 등록할 때 사용
    @EnableConfigurationProperties(MyAwsConfig.MyAwsPropertiesV2.class)
    public class MyAwsConfig {
    
        private final MyAwsPropertiesV2 properties;
    
        public MyAwsConfig(MyAwsPropertiesV2 properties) {
            this.properties = properties;
        }
    
        @Bean
        public MyAwsSource myAwsSource(){
            return new MyAwsSource(
                    properties.getAccessKey(),
                    properties.getSecretKey(),
                    properties.getRegion(),
                    properties.getBucket(),
                    properties.getPresignedUrlExpiration()
            );
        }
    

 

완성 코드

@Configuration
@ConditionalOnProperty(name = "discodeit.storage.type", havingValue = "s3")
@EnableConfigurationProperties(S3ConfigProperties.class)
@RequiredArgsConstructor
public class S3Config {

    private final S3ConfigProperties properties;

    @Bean
    public S3Client s3Client() {
        AwsBasicCredentials credentials = AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey());
        return S3Client.builder()
                .region(Region.of(properties.getRegion()))
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .build();
        //
    }

    @Getter
    @AllArgsConstructor
    @ConfigurationProperties(prefix = "discodeit.storage.s3")
    @Validated
    public static class S3ConfigProperties {
        @NotBlank (message = "Access key is required")
        private String accessKey;
        @NotBlank (message = "Secret key is required")
        private String secretKey;
        @NotBlank (message = "Region is required")
        private String region;
        @NotBlank (message = "Bucket is required")
        private String bucket;
        @NotBlank (message = "Presigned-url-expiration is required")
        private long presignedUrlExpiration;
    }
}

 

S3ConfigProperties는 PropertiesScan되면 자동으로 빈 등록됨