스프링 애플리케이션을 개발하다 보면 설정값을 다루는 코드가 여기저기 흩어지기 마련이다. AWS 키, 유효성 검증 규칙, 관리자 정보... 이런 값들을 어떻게 관리하느냐에 따라 코드의 유지보수성이 크게 달라진다.
@Value의 한계
@Value는 스프링에서 설정값을 주입받는 가장 기본적인 방법이다.
@Component
public class StorageConfig {
@Value("${aws.s3.access-key}")
private String accessKey;
@Value("${aws.s3.secret-key}")
private String secretKey;
@Value("${aws.s3.region}")
private String region;
}
간단해 보이지만 실제 프로젝트에서는 문제가 금방 드러난다.
타입 안전성 부재
@Value는 문자열 기반이다. ${aws.s3.regoin}처럼 오타가 있어도 컴파일 시점에는 아무 경고가 없다. 애플리케이션을 띄워야 비로소 실패하고, 그마저도 기본값이 설정돼 있으면 잘못된 값으로 조용히 동작한다.
설정 분산
하나의 논리적 설정 그룹(예: AWS S3 관련 설정)이 여러 클래스에 흩어질 수 있다. 설정 키가 바뀌면 프로젝트 전체를 뒤져야 한다.
검증 불가
@Value만으로는 주입된 값이 유효한지 검증할 방법이 없다. 비밀번호 최소 길이가 음수여도 그대로 주입된다.
@ConfigurationProperties로 전환하기
@ConfigurationProperties는 설정값을 타입 안전한 자바 객체로 바인딩한다. 이것이 스프링 팀이 공식적으로 권장하는 방식이다.
@ConfigurationProperties(prefix = "aws.s3")
public class AwsS3Properties {
private String accessKey;
private String secretKey;
private String region;
private String bucket;
private Long presignedUrlExpiration;
// getter, setter
}
prefix = "aws.s3"—application.yml의aws.s3아래 프로퍼티들이 자동으로 매핑된다.- 필드명과 yml 키가 자동으로 매칭된다.
accessKey→access-key(relaxed binding).
활성화 방법
@ConfigurationProperties 클래스를 스프링이 인식하려면 두 가지 방법 중 하나를 택한다.
// 방법 1: 메인 클래스나 설정 클래스에서 스캔
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application { }
// 방법 2: 특정 프로퍼티 클래스를 명시적으로 등록
@Configuration
@EnableConfigurationProperties(AwsS3Properties.class)
public class AppConfig { }
사용하는 쪽
@Configuration
@RequiredArgsConstructor
public class S3Config {
private final AwsS3Properties awsS3Properties;
@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.of(awsS3Properties.getRegion()))
.credentialsProvider(credentialsProvider())
.build();
}
private StaticCredentialsProvider credentialsProvider() {
return StaticCredentialsProvider.create(
AwsBasicCredentials.create(
awsS3Properties.getAccessKey(),
awsS3Properties.getSecretKey()));
}
}
@Value어노테이션이 완전히 사라졌다.- 설정값의 출처가
AwsS3Properties하나로 집중된다. - 생성자 주입으로 불변성까지 보장할 수 있다.
불변 프로퍼티 클래스
스프링 부트 2.2+부터는 @ConstructorBinding을 사용해 불변 프로퍼티 클래스를 만들 수 있다. 스프링 부트 3.x에서는 생성자가 하나뿐이면 자동으로 생성자 바인딩이 적용된다.
@ConfigurationProperties(prefix = "aws.s3")
public record AwsS3Properties(
String accessKey,
String secretKey,
String region,
String bucket,
Long presignedUrlExpiration
) { }
- Java
record를 사용하면 getter, equals, hashCode, toString이 자동 생성된다. - 필드가
final이므로 런타임에 값이 바뀔 위험이 없다.
유효성 검증 통합
@ConfigurationProperties의 진짜 강점은 Bean Validation과 결합할 수 있다는 점이다.
@Validated
@ConfigurationProperties(prefix = "aws.s3")
public record AwsS3Properties(
@NotBlank String accessKey,
@NotBlank String secretKey,
@NotBlank String region,
@NotBlank String bucket,
@Positive Long presignedUrlExpiration
) { }
- 애플리케이션 시작 시점에 설정값이 검증된다. 빈 문자열이나 음수가 들어오면 즉시 실패한다.
- "Fast Fail" 원칙 — 잘못된 설정은 배포 전에 잡는 것이 가장 좋다.
***************************
APPLICATION FAILED TO START
***************************
Binding to target AwsS3Properties failed:
Property: aws.s3.access-key
Value:
Reason: must not be blank
설정 그룹화 전략
프로젝트 규모가 커지면 설정값도 많아진다. 이때 어떤 기준으로 프로퍼티 클래스를 나눌지가 중요하다.
핵심 원칙은 하나의 프로퍼티 클래스가 하나의 설정 관심사를 담당하는 것이다.
AwsS3Properties— S3 접속 정보와 버킷 설정AdminProperties— 초기 관리자 계정 정보ValidationProperties— 사용자 입력 검증 규칙SecurityProperties— Remember-Me 같은 보안 관련 설정
이렇게 나누면 특정 설정을 변경할 때 영향 범위가 명확해진다.
심화 분석
@Value와 @ConfigurationProperties 비교
@Value는 SpEL(Spring Expression Language)을 지원한다. 단순 주입을 넘어 #{systemProperties['user.home']}이나 ${value:default} 같은 표현식을 사용할 수 있다. 이것이 @Value가 여전히 유용한 유일한 시나리오다.
반면 @ConfigurationProperties는 다음을 추가로 제공한다.
- Relaxed Binding —
camelCase,kebab-case,UPPER_CASE모두 자동 매핑 - 중첩 객체 —
@NestedConfigurationProperties로 계층형 설정 표현 가능 - IDE 지원 —
spring-boot-configuration-processor를 추가하면 yml 편집 시 자동완성 제공 - 프로파일별 오버라이드 — 프로퍼티 클래스 자체가 빈이므로,
@Profile과 자연스럽게 결합
spring-boot-configuration-processor
dependencies {
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}
이 의존성을 추가하면 빌드 시 META-INF/spring-configuration-metadata.json이 생성되어, IDE에서 application.yml 편집 시 프로퍼티 자동완성과 타입 힌트를 사용할 수 있다.
자주 하는 실수
static 필드에 @Value setter로 주입
@Component
public class ValidationProperties {
public static int ID_MIN_LENGTH;
@Value("${validation.user.id.min-length}")
public void setIdMinLength(int value) {
ID_MIN_LENGTH = value; // static 필드에 주입
}
}
이 패턴은 세 가지 문제가 있다.
- 멀티스레드 불안전 — static 필드 쓰기에 동기화가 없다. 빈 초기화 시점에 다른 스레드가 이 값을 읽으면 0(int 기본값)을 볼 수 있다.
- 테스트 어려움 — static 필드는 테스트 간에 상태가 공유되어 테스트 격리가 깨진다.
- 스프링 생명주기와 충돌 —
@Valuesetter가 호출되기 전에 static 필드를 참조하는 코드가 실행될 수 있다.
올바른 해결책은 @ConfigurationProperties로 전환하고, 해당 빈을 필요한 곳에 주입하는 것이다.
하드코딩된 설정값
public class OnlineThreshold {
public static final Duration ONLINE_THRESHOLD = Duration.ofMinutes(5);
}
application.yml에 설정값이 있는데도 하드코딩하는 경우가 있다. 이렇게 하면 환경별로 다른 값을 적용할 수 없고, 설정 변경 시 재컴파일이 필요하다.
user-status:
online-threshold-minutes: 5
이미 yml에 값이 정의되어 있다면, @ConfigurationProperties로 읽어와야 한다.
prefix 누락
@ConfigurationProperties // prefix가 없으면 모든 프로퍼티를 매핑 시도
public class AppProperties { }
반드시 prefix를 지정해서 매핑 범위를 한정해야 한다.