기존에 필드값 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}") // 먼저 ${}로 값 가져오고, 그걸로 연산
하지만, 가독성이 너무 떨어져서 최대한 분리할 수 있으면 분리하는 것이 좋은 것 같다.
✅ 어디에 쓸 수 있는가?
@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
public @interface Value
- 필드값
- 가장 흔하게 쓰는 방식이다.
@Value("${discodeit.storage.s3.presigned-url-expiration}") private final Long presinged_url_expiration;
- 메서드/생성자 파라미터 값
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)
- 애노테이션 내부
✅ 작동 원리 - 언제, 어떻게 처리
@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방법은 따로 파일을 두어 빈으로 등록하는 방법
✅ 관련 애노테이션
- @ConfiguratoinPropertiesScan : 자동 스캔을 지원한다.
[애플리케이션 실행 메서드에 붙이면됨] @ConfigurationPropertiesScan public class ExternalReadApplication { public static void main(String[] args) { SpringApplication.run(ExternalReadApplication.class, args); }
- @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되면 자동으로 빈 등록됨
'백엔드 > 스프링' 카테고리의 다른 글
S3Resource사용 : 순환참조 오류 (0) | 2025.04.23 |
---|---|
Spring-batch 테스트 : 여러 JOB에서 생기는 오류 NoUniqueBeanDefinitionException (0) | 2025.04.22 |
Chunk 지향 프로세싱 + Tasklet (0) | 2025.04.07 |
스프링 배치 - Chunk (preview) (0) | 2025.04.07 |
스프링 배치 - StepContribution (0) | 2025.04.06 |