Spring @ConfigurationProperties란?
Spring Boot에서 @Value로 설정값을 주입하는 방식은 단순하지만, 설정이 많아지면 관리가 어렵고 타입 안전성도 떨어진다. @ConfigurationProperties는 설정값을 타입 안전한 POJO에 바인딩하여 이 문제를 해결한다.
# application.yml
app:
mail:
host: smtp.gmail.com
port: 587
from: noreply@example.com
enabled: true
retry:
max-attempts: 3
delay-ms: 1000
// ❌ @Value: 흩어진 설정, 타입 불안전
@Service
public class MailService {
@Value("${app.mail.host}") String host;
@Value("${app.mail.port}") int port; // 오타 시 런타임 에러
@Value("${app.mail.from}") String from;
}
// ✅ @ConfigurationProperties: 타입 안전, 한 곳에 집중
@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(
String host,
int port,
String from,
boolean enabled,
Retry retry
) {
public record Retry(int maxAttempts, long delayMs) {}
}
활성화 방법 3가지
ConfigurationProperties를 스캔하는 방법은 세 가지다:
1. @EnableConfigurationProperties (명시적)
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfig {
@Bean
public JavaMailSender mailSender(MailProperties props) {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost(props.host());
sender.setPort(props.port());
return sender;
}
}
2. @ConfigurationPropertiesScan (패키지 스캔)
@SpringBootApplication
@ConfigurationPropertiesScan("com.example.config")
public class Application { }
3. @Component (간편하지만 비권장)
// 동작하지만, Properties 클래스가 컴포넌트 스캔에 의존하게 됨
@Component
@ConfigurationProperties(prefix = "app.mail")
public class MailProperties { ... }
권장: @ConfigurationPropertiesScan이 가장 깔끔하다. Properties 클래스에 Spring 어노테이션을 붙이지 않아도 된다.
Record 기반 Immutable Properties
Spring Boot 3.x부터 Java Record를 사용한 불변 바인딩이 권장된다:
@ConfigurationProperties(prefix = "app.storage")
public record StorageProperties(
String basePath,
long maxFileSizeMb,
List<String> allowedExtensions,
Map<String, BucketConfig> buckets
) {
public record BucketConfig(
String name,
String region,
boolean publicAccess
) {}
}
// 사용
@Service
@RequiredArgsConstructor
public class StorageService {
private final StorageProperties props;
public void upload(MultipartFile file) {
if (file.getSize() > props.maxFileSizeMb() * 1024 * 1024) {
throw new FileTooLargeException();
}
BucketConfig bucket = props.buckets().get("images");
// ...
}
}
Record는 생성자 바인딩을 사용하므로 @ConstructorBinding 없이도 불변 객체가 된다 (Boot 3.x).
검증: @Validated + Jakarta Validation
설정값 검증은 @Validated와 Jakarta Validation 어노테이션으로 수행한다. 잘못된 설정이면 애플리케이션 시작 시점에 즉시 실패한다:
@Validated
@ConfigurationProperties(prefix = "app.jwt")
public record JwtProperties(
@NotBlank String secret,
@Min(60) @Max(86400) long accessTokenTtlSeconds,
@Min(3600) long refreshTokenTtlSeconds,
@NotBlank String issuer
) {
// 커스텀 검증: refresh > access
@AssertTrue(message = "refreshTokenTtl must be greater than accessTokenTtl")
boolean isRefreshGreaterThanAccess() {
return refreshTokenTtlSeconds > accessTokenTtlSeconds;
}
}
# 잘못된 설정 시 시작 실패 메시지:
# Binding validation errors on app.jwt
# - accessTokenTtlSeconds: must be greater than or equal to 60
# - secret: must not be blank
핵심: 런타임이 아닌 시작 시점에 설정 오류를 잡는다. 프로덕션 배포 후 설정 누락으로 장애가 나는 것을 방지한다.
중첩 구조와 컬렉션 바인딩
# application.yml
app:
datasource:
primary:
url: jdbc:postgresql://primary:5432/db
pool-size: 20
replicas:
- url: jdbc:postgresql://replica1:5432/db
pool-size: 10
- url: jdbc:postgresql://replica2:5432/db
pool-size: 10
connection-timeout: 5s
idle-timeout: 10m
@ConfigurationProperties(prefix = "app.datasource")
public record DataSourceProperties(
DbConfig primary,
List<DbConfig> replicas,
Duration connectionTimeout, // "5s" → Duration 자동 변환
Duration idleTimeout // "10m" → Duration 자동 변환
) {
public record DbConfig(String url, int poolSize) {}
}
Spring Boot는 Duration, DataSize, Period 타입을 자동 변환한다. "5s", "10m", "2h", "100MB" 등 사람이 읽기 쉬운 형식을 사용할 수 있다.
Profile별 설정 오버라이드
Spring Profiles와 결합하여 환경별 설정을 관리한다:
# application.yml (기본값)
app:
cache:
ttl: 10m
max-size: 1000
# application-prod.yml (프로덕션 오버라이드)
app:
cache:
ttl: 1h
max-size: 50000
기본값을 application.yml에 두고, 환경별 차이만 프로파일 파일에 오버라이드하는 패턴이 가장 유지보수하기 좋다.
테스트에서 설정 주입
// 방법 1: @TestPropertySource
@SpringBootTest
@TestPropertySource(properties = {
"app.mail.host=localhost",
"app.mail.port=2525"
})
class MailServiceTest { ... }
// 방법 2: @DynamicPropertySource (Testcontainers와 조합)
@SpringBootTest
class MailServiceTest {
@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("app.mail.host", mailContainer::getHost);
registry.add("app.mail.port", mailContainer::getSmtpPort);
}
}
Custom Starter에서 활용
Custom Starter를 만들 때 ConfigurationProperties는 핵심이다:
// starter의 Properties
@ConfigurationProperties(prefix = "mylib.rate-limiter")
public record RateLimiterProperties(
@DefaultValue("100") int maxRequests,
@DefaultValue("1m") Duration window,
@DefaultValue("true") boolean enabled
) {}
// AutoConfiguration에서 조건부 빈 등록
@AutoConfiguration
@EnableConfigurationProperties(RateLimiterProperties.class)
@ConditionalOnProperty(prefix = "mylib.rate-limiter", name = "enabled",
havingValue = "true", matchIfMissing = true)
public class RateLimiterAutoConfiguration {
@Bean
public RateLimiter rateLimiter(RateLimiterProperties props) {
return new SlidingWindowRateLimiter(
props.maxRequests(), props.window()
);
}
}
@DefaultValue로 Record 필드의 기본값을 지정할 수 있다. 사용자가 설정을 생략해도 합리적인 기본값으로 동작한다.
@Value vs @ConfigurationProperties
| 항목 | @Value | @ConfigurationProperties |
|---|---|---|
| 타입 안전성 | 문자열 기반, 런타임 에러 | 컴파일 타임 타입 체크 |
| 중첩 구조 | 불가 | 자연스러운 객체 매핑 |
| 검증 | 수동 | @Validated 자동 검증 |
| SpEL 지원 | 지원 | 미지원 |
| 적합한 경우 | 단일 값, SpEL 필요 시 | 구조화된 설정 그룹 |
정리
@ConfigurationProperties는 Spring Boot 설정 관리의 표준이다. Record 기반 불변 바인딩으로 타입 안전성을 확보하고, @Validated로 시작 시점에 설정 오류를 잡고, 중첩 구조와 Duration/DataSize 자동 변환으로 복잡한 설정도 깔끔하게 관리할 수 있다. @Value는 단일 값에만 사용하고, 구조화된 설정은 반드시 ConfigurationProperties를 사용하라.