Spring ConfigurationProperties 심화

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를 사용하라.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux