application.yml에 적은 값이 어떻게 자바 코드의 변수에 들어가게 되는 걸까요? @Value@ConfigurationProperties는 무엇이 다르고, 언제 어떤 것을 써야 할까요?

개념 정의

스프링부트의 외부 설정(Externalized Configuration) 은 코드 변경 없이 애플리케이션의 동작을 설정 파일, 환경 변수, 커맨드라인 인자 등으로 제어하는 메커니즘입니다. 설정 값을 코드에 주입하는 방법으로 @Value@ConfigurationProperties가 있습니다.

왜 필요한가

설정 값을 코드에 하드코딩하면 환경마다 빌드를 새로 해야 합니다.

JAVA
// 나쁜 예: 하드코딩
public class ApiClient {
    private String baseUrl = "https://api.example.com"; // 환경마다 다른데?
    private int timeout = 30; // 변경하려면 재배포?
}

외부 설정을 사용하면 빌드 산출물 하나로 모든 환경에서 실행할 수 있습니다.

내부 동작

@Value — 단순 값 주입

JAVA
@Service
public class ApiClient {

    @Value("${api.base-url}")
    private String baseUrl;

    @Value("${api.timeout:30}") // 기본값 30
    private int timeout;

    @Value("${api.enabled:true}")
    private boolean enabled;

    @Value("#{${api.timeout} * 1000}") // SpEL 표현식
    private long timeoutMillis;
}
YAML
api:
  base-url: https://api.example.com
  timeout: 10
  enabled: true

@Value의 한계:

  • 프로퍼티 이름이 문자열이라 오타를 컴파일러가 못 잡습니다
  • 관련 프로퍼티를 하나로 묶기 어렵습니다
  • 유효성 검증이 안 됩니다

@ConfigurationProperties — 타입 안전 바인딩

JAVA
@ConfigurationProperties(prefix = "api")
public class ApiProperties {
    private String baseUrl;
    private int timeout = 30; // 기본값
    private boolean enabled = true;
    private Retry retry = new Retry();

    // getter, setter

    public static class Retry {
        private int maxAttempts = 3;
        private Duration delay = Duration.ofSeconds(1);
        // getter, setter
    }
}
YAML
api:
  base-url: https://api.example.com
  timeout: 10
  retry:
    max-attempts: 5
    delay: 2s
JAVA
// 사용하는 곳
@Service
@RequiredArgsConstructor
public class ApiClient {
    private final ApiProperties properties;

    public void call() {
        String url = properties.getBaseUrl();
        int timeout = properties.getTimeout();
        int maxRetries = properties.getRetry().getMaxAttempts();
    }
}

@ConfigurationProperties 활성화

JAVA
// 방법 1: @EnableConfigurationProperties
@Configuration
@EnableConfigurationProperties(ApiProperties.class)
public class AppConfig {}

// 방법 2: @ConfigurationPropertiesScan (패키지 스캔)
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {}

// 방법 3: @Component (빈으로 직접 등록)
@Component
@ConfigurationProperties(prefix = "api")
public class ApiProperties { ... }

Relaxed Binding

스프링부트는 다양한 프로퍼티 표기법을 자동으로 매핑합니다.

PLAINTEXT
YAML/properties          자바 필드          환경 변수
────────────────────────────────────────────────────────
api.base-url          → baseUrl          ← API_BASE_URL
api.baseUrl           → baseUrl
api.base_url          → baseUrl
api.BASE-URL          → baseUrl

환경 변수에서는 점(.)을 밑줄(_)로, 하이픈(-)도 밑줄로, 모두 대문자로 변환합니다.

코드 예제

유효성 검증

JAVA
@Validated
@ConfigurationProperties(prefix = "api")
public class ApiProperties {

    @NotBlank
    private String baseUrl;

    @Min(1)
    @Max(300)
    private int timeout = 30;

    @NotNull
    private Duration connectionTimeout;

    // getter, setter
}

설정 값이 유효하지 않으면 애플리케이션 시작 시 즉시 에러 가 발생합니다.

PLAINTEXT
***************************
APPLICATION FAILED TO START
***************************
Binding to target ApiProperties failed:

    Property: api.base-url
    Value: ""
    Reason: 공백일 수 없습니다

Duration과 DataSize 바인딩

JAVA
@ConfigurationProperties(prefix = "server")
public class ServerProperties {
    private Duration readTimeout;       // 30s, 5m, 1h
    private Duration writeTimeout;
    private DataSize maxRequestSize;    // 10MB, 1GB
}
YAML
server:
  read-timeout: 30s
  write-timeout: 5m
  max-request-size: 10MB

지원하는 단위:

  • Duration: ns, us, ms, s, m, h, d
  • DataSize: B, KB, MB, GB, TB

List와 Map 바인딩

JAVA
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private List<String> allowedOrigins;
    private Map<String, String> headers;
}
YAML
app:
  allowed-origins:
    - https://example.com
    - https://app.example.com
  headers:
    X-Api-Key: my-key
    X-Client-Id: web-app

불변 @ConfigurationProperties (레코드 사용)

JAVA
@ConfigurationProperties(prefix = "api")
public record ApiProperties(
    @NotBlank String baseUrl,
    @DefaultValue("30") int timeout,
    @DefaultValue Retry retry
) {
    public record Retry(
        @DefaultValue("3") int maxAttempts,
        @DefaultValue("1s") Duration delay
    ) {}
}

생성자 바인딩을 사용하면 setter 없이 불변 객체로 프로퍼티를 관리할 수 있습니다.

@Value vs @ConfigurationProperties 비교

기준@Value@ConfigurationProperties
타입 안전SpEL 문자열자바 타입 바인딩
유효성 검증불가@Validated 지원
관련 속성 묶기불편클래스로 구조화
Relaxed Binding제한적완전 지원
SpEL 표현식지원미지원
사용 추천단순 값 1~2개관련 설정 그룹

프로퍼티 암호화

민감한 설정 값은 암호화해서 관리할 수 있습니다.

YAML
# jasypt-spring-boot 사용 시
spring:
  datasource:
    password: ENC(암호화된문자열)

또는 환경 변수로 민감 정보를 분리합니다.

YAML
spring:
  datasource:
    password: ${DB_PASSWORD} # 환경 변수에서 주입

프로퍼티 소스 커스텀 등록

JAVA
@Configuration
@PropertySource(value = "classpath:custom.yml",
                factory = YamlPropertySourceFactory.class)
public class CustomConfig {}

public class YamlPropertySourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) {
        YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
        factory.setResources(resource.getResource());
        Properties properties = factory.getObject();
        return new PropertiesPropertySource(
            resource.getResource().getFilename(), properties);
    }
}

주의할 점

1. @Value에서 프로퍼티가 없고 기본값도 없으면 애플리케이션이 시작되지 않는다

@Value("${api.key}")에서 api.key 프로퍼티가 환경에 없으면 IllegalArgumentException: Could not resolve placeholder로 시작이 실패합니다. 환경변수로 주입하는 민감 정보를 로컬에서 설정하지 않은 채 실행하면 즉시 발생합니다. @Value("${api.key:}")처럼 기본값을 지정하거나, @ConfigurationProperties@Validated를 사용하여 시작 시 명확한 에러 메시지를 제공하세요.

2. 환경 변수가 yml 설정을 덮어쓰는 것을 모르면 디버깅이 어렵다

환경 변수는 application.yml보다 우선순위가 높습니다. CI/CD에서 SPRING_DATASOURCE_URL 환경 변수가 설정되어 있으면 yml의 spring.datasource.url이 무시됩니다. yml을 아무리 수정해도 반영되지 않는 이유를 모르면 수 시간을 삽질합니다. --debug 플래그로 어떤 프로퍼티 소스에서 값이 로드되는지 확인하세요.

3. @ConfigurationProperties에 setter가 없으면 바인딩이 실패한다

@ConfigurationProperties는 기본적으로 setter를 통해 값을 바인딩합니다. Lombok의 @Getter만 있고 @Setter가 없으면 프로퍼티 값이 주입되지 않아 모든 필드가 기본값으로 남습니다. 에러 없이 조용히 실패하므로 찾기 어렵습니다. 불변 객체를 원하면 Java record나 생성자 바인딩(@ConstructorBinding)을 사용하세요.

정리

항목설명
@Value단순 값 1~2개 주입. SpEL 지원, 유효성 검증 불가
@ConfigurationProperties관련 설정 그룹화. 타입 안전, 유효성 검증, Relaxed Binding
타입 변환Duration(30s), DataSize(10MB) 자동 변환
민감 정보환경 변수(${DB_PASSWORD})로 분리 또는 jasypt 암호화
우선순위커맨드라인 > 환경변수 > 프로파일 yml > 기본 yml
댓글 로딩 중...