외부 설정 — application.yml 값은 어떻게 코드에 주입될까
application.yml에 적은 값이 어떻게 자바 코드의 변수에 들어가게 되는 걸까요?
@Value와@ConfigurationProperties는 무엇이 다르고, 언제 어떤 것을 써야 할까요?
개념 정의
스프링부트의 외부 설정(Externalized Configuration) 은 코드 변경 없이 애플리케이션의 동작을 설정 파일, 환경 변수, 커맨드라인 인자 등으로 제어하는 메커니즘입니다. 설정 값을 코드에 주입하는 방법으로 @Value와 @ConfigurationProperties가 있습니다.
왜 필요한가
설정 값을 코드에 하드코딩하면 환경마다 빌드를 새로 해야 합니다.
// 나쁜 예: 하드코딩
public class ApiClient {
private String baseUrl = "https://api.example.com"; // 환경마다 다른데?
private int timeout = 30; // 변경하려면 재배포?
}
외부 설정을 사용하면 빌드 산출물 하나로 모든 환경에서 실행할 수 있습니다.
내부 동작
@Value — 단순 값 주입
@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;
}
api:
base-url: https://api.example.com
timeout: 10
enabled: true
@Value의 한계:
- 프로퍼티 이름이 문자열이라 오타를 컴파일러가 못 잡습니다
- 관련 프로퍼티를 하나로 묶기 어렵습니다
- 유효성 검증이 안 됩니다
@ConfigurationProperties — 타입 안전 바인딩
@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
}
}
api:
base-url: https://api.example.com
timeout: 10
retry:
max-attempts: 5
delay: 2s
// 사용하는 곳
@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 활성화
// 방법 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
스프링부트는 다양한 프로퍼티 표기법을 자동으로 매핑합니다.
YAML/properties 자바 필드 환경 변수
────────────────────────────────────────────────────────
api.base-url → baseUrl ← API_BASE_URL
api.baseUrl → baseUrl
api.base_url → baseUrl
api.BASE-URL → baseUrl
환경 변수에서는 점(.)을 밑줄(_)로, 하이픈(-)도 밑줄로, 모두 대문자로 변환합니다.
코드 예제
유효성 검증
@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
}
설정 값이 유효하지 않으면 애플리케이션 시작 시 즉시 에러 가 발생합니다.
***************************
APPLICATION FAILED TO START
***************************
Binding to target ApiProperties failed:
Property: api.base-url
Value: ""
Reason: 공백일 수 없습니다
Duration과 DataSize 바인딩
@ConfigurationProperties(prefix = "server")
public class ServerProperties {
private Duration readTimeout; // 30s, 5m, 1h
private Duration writeTimeout;
private DataSize maxRequestSize; // 10MB, 1GB
}
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 바인딩
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private List<String> allowedOrigins;
private Map<String, String> headers;
}
app:
allowed-origins:
- https://example.com
- https://app.example.com
headers:
X-Api-Key: my-key
X-Client-Id: web-app
불변 @ConfigurationProperties (레코드 사용)
@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개 | 관련 설정 그룹 |
프로퍼티 암호화
민감한 설정 값은 암호화해서 관리할 수 있습니다.
# jasypt-spring-boot 사용 시
spring:
datasource:
password: ENC(암호화된문자열)
또는 환경 변수로 민감 정보를 분리합니다.
spring:
datasource:
password: ${DB_PASSWORD} # 환경 변수에서 주입
프로퍼티 소스 커스텀 등록
@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 |