Spring PropertySource란?
Spring의 PropertySource 추상화는 애플리케이션 설정을 다양한 외부 소스에서 로딩하고 우선순위 기반으로 병합하는 핵심 메커니즘입니다. application.yml만 쓰고 있다면 Spring 설정 시스템의 10%만 활용하고 있는 셈입니다.
이 글에서는 PropertySource의 내부 우선순위 체계, 커스텀 PropertySource 구현, EnvironmentPostProcessor로 외부 소스 통합, 그리고 AWS SSM·Vault 연동 패턴까지 심층적으로 다룹니다.
PropertySource 우선순위 체계
Spring Boot는 17단계 이상의 PropertySource를 순서대로 로딩하며, 먼저 로딩된 값이 우선합니다. 핵심 순서를 정리하면:
// 우선순위 높음 → 낮음
1. @TestPropertySource (테스트 전용)
2. 커맨드라인 인수 (--server.port=9090)
3. SPRING_APPLICATION_JSON 환경변수
4. ServletConfig / ServletContext 파라미터
5. JNDI (java:comp/env)
6. System.getProperties() (JVM 시스템 프로퍼티)
7. OS 환경변수
8. random.* 프로퍼티
9. 외부 application-{profile}.yml
10. 내부 application-{profile}.yml
11. 외부 application.yml
12. 내부 application.yml
13. @PropertySource 어노테이션
14. SpringApplication.setDefaultProperties()
이 순서를 이해하면 환경변수로 yml 값을 오버라이드하거나, 프로파일별 설정 분리가 왜 동작하는지 명확해집니다.
@PropertySource 커스텀 설정 파일
기본 application.yml 외에 추가 설정 파일을 로딩하려면 @PropertySource를 사용합니다:
@Configuration
@PropertySource("classpath:payment.properties")
@PropertySource(
value = "classpath:payment-${spring.profiles.active}.properties",
ignoreResourceNotFound = true // 파일 없어도 에러 안 남
)
public class PaymentConfig {
@Value("${payment.gateway.url}")
private String gatewayUrl;
@Value("${payment.timeout:5000}") // 기본값 지정
private int timeout;
}
주의: @PropertySource는 기본적으로 .properties 형식만 지원합니다. YAML을 로딩하려면 커스텀 팩토리가 필요합니다:
// YAML PropertySource 팩토리
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(
String name, EncodedResource resource) throws IOException {
var factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
var properties = factory.getObject();
String sourceName = name != null ? name
: resource.getResource().getFilename();
return new PropertiesPropertySource(sourceName, properties);
}
}
// 사용
@Configuration
@PropertySource(
value = "classpath:payment.yml",
factory = YamlPropertySourceFactory.class
)
public class PaymentConfig { ... }
EnvironmentPostProcessor: 부팅 시점 설정 주입
EnvironmentPostProcessor는 Spring Boot 부팅 초기에 Environment를 조작할 수 있는 강력한 확장 포인트입니다. Bean이 생성되기 전에 실행되므로, 외부 소스에서 설정을 가져와 주입하기에 최적입니다:
// AWS SSM Parameter Store에서 설정 로딩
public class SsmEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(
ConfigurableEnvironment environment,
SpringApplication application) {
// 프로파일 확인 — 로컬에서는 SSM 스킵
if (environment.acceptsProfiles(Profiles.of("local"))) {
return;
}
String prefix = environment.getProperty(
"app.ssm.prefix", "/myapp/production"
);
var ssmClient = SsmClient.builder()
.region(Region.AP_NORTHEAST_2)
.build();
var response = ssmClient.getParametersByPath(r -> r
.path(prefix)
.withDecryption(true)
.recursive(true)
);
var properties = new Properties();
for (var param : response.parameters()) {
// /myapp/production/db.url → db.url
String key = param.name()
.replace(prefix + "/", "")
.replace("/", ".");
properties.put(key, param.value());
}
// 높은 우선순위로 등록
environment.getPropertySources().addFirst(
new PropertiesPropertySource("aws-ssm", properties)
);
}
}
// META-INF/spring.factories 등록
// Spring Boot 2.x
org.springframework.boot.env.EnvironmentPostProcessor=
com.example.SsmEnvironmentPostProcessor
// Spring Boot 3.x — META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports
com.example.SsmEnvironmentPostProcessor
커스텀 PropertySource 구현
데이터베이스나 외부 API에서 동적으로 설정을 로딩하는 커스텀 PropertySource를 만들 수 있습니다:
// DB 기반 동적 PropertySource
public class DatabasePropertySource extends PropertySource<DataSource> {
private final JdbcTemplate jdbc;
private final Map<String, String> cache = new ConcurrentHashMap<>();
private volatile long lastRefresh = 0;
private static final long REFRESH_INTERVAL = 60_000; // 1분
public DatabasePropertySource(DataSource dataSource) {
super("database-properties", dataSource);
this.jdbc = new JdbcTemplate(dataSource);
refresh();
}
@Override
public Object getProperty(String name) {
refreshIfStale();
return cache.get(name);
}
private void refreshIfStale() {
if (System.currentTimeMillis() - lastRefresh > REFRESH_INTERVAL) {
refresh();
}
}
private synchronized void refresh() {
try {
var rows = jdbc.queryForList(
"SELECT prop_key, prop_value FROM app_config WHERE active = true"
);
Map<String, String> newCache = new HashMap<>();
for (var row : rows) {
newCache.put(
(String) row.get("prop_key"),
(String) row.get("prop_value")
);
}
cache.clear();
cache.putAll(newCache);
lastRefresh = System.currentTimeMillis();
} catch (Exception e) {
// 실패 시 기존 캐시 유지
log.warn("Failed to refresh DB properties", e);
}
}
}
// 등록
@Configuration
public class DynamicPropertyConfig {
@Bean
public static BeanFactoryPostProcessor dbPropertySourceRegistrar(
ConfigurableEnvironment env, DataSource ds) {
return beanFactory -> {
env.getPropertySources().addLast(
new DatabasePropertySource(ds)
);
};
}
}
Vault 통합: 시크릿 자동 주입
HashiCorp Vault와 통합하면 시크릿을 코드나 yml에 하드코딩하지 않고 런타임에 주입할 수 있습니다:
// build.gradle
implementation 'org.springframework.vault:spring-vault-core:3.1.0'
// VaultEnvironmentPostProcessor
public class VaultEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(
ConfigurableEnvironment env, SpringApplication app) {
if (env.acceptsProfiles(Profiles.of("local"))) return;
String vaultAddr = env.getProperty("vault.addr", "http://vault:8200");
String vaultToken = env.getProperty("vault.token");
String secretPath = env.getProperty("vault.secret-path", "secret/data/myapp");
var endpoint = VaultEndpoint.from(URI.create(vaultAddr));
var template = new VaultTemplate(endpoint, () -> VaultToken.of(vaultToken));
var response = template.read(secretPath);
if (response != null && response.getData() != null) {
Map<String, Object> data = response.getData();
var properties = new Properties();
data.forEach((k, v) -> properties.put(k, v.toString()));
env.getPropertySources().addFirst(
new PropertiesPropertySource("vault-secrets", properties)
);
}
}
}
# application.yml — Vault에서 주입된 값 사용
spring:
datasource:
url: jdbc:postgresql://db:5432/myapp
username: ${db.username} # Vault에서 주입
password: ${db.password} # Vault에서 주입
payment:
api-key: ${payment.api-key} # Vault에서 주입
설정 새로고침: @RefreshScope
Spring Cloud를 사용하면 애플리케이션 재시작 없이 설정을 동적으로 갱신할 수 있습니다:
@RestController
@RefreshScope // /actuator/refresh 호출 시 빈 재생성
public class FeatureFlagController {
@Value("${feature.new-ui:false}")
private boolean newUiEnabled;
@Value("${rate-limit.max-requests:100}")
private int maxRequests;
@GetMapping("/features")
public Map<String, Object> features() {
return Map.of(
"newUi", newUiEnabled,
"maxRequests", maxRequests
);
}
}
// 갱신 트리거
// POST /actuator/refresh → 변경된 프로퍼티 목록 반환
커스텀 PropertySource와 @RefreshScope를 조합하면 DB 설정 변경 → Actuator refresh → 즉시 반영 파이프라인을 구축할 수 있습니다.
PropertySource 우선순위 디버깅
설정 값이 예상과 다를 때 어떤 PropertySource에서 로딩됐는지 확인하는 방법:
// 1) Actuator /env 엔드포인트
// GET /actuator/env/spring.datasource.url
// → 어떤 PropertySource에서 왔는지 표시
// 2) 코드에서 직접 확인
@Component
@RequiredArgsConstructor
public class PropertyDebugger implements CommandLineRunner {
private final ConfigurableEnvironment env;
@Override
public void run(String... args) {
// 모든 PropertySource 이름과 순서 출력
env.getPropertySources().forEach(ps ->
System.out.println("[PropertySource] " + ps.getName()
+ " (" + ps.getClass().getSimpleName() + ")")
);
// 특정 키가 어디서 왔는지 추적
String key = "spring.datasource.url";
for (var ps : env.getPropertySources()) {
if (ps.containsProperty(key)) {
System.out.printf("'%s' resolved from: %s = %s%n",
key, ps.getName(), ps.getProperty(key));
break; // 첫 번째 매칭이 최종 값
}
}
}
}
프로덕션 설정 아키텍처 패턴
| 설정 유형 | 저장 위치 | 갱신 방법 |
|---|---|---|
| 앱 기본값 | application.yml |
배포 시 |
| 환경별 설정 | application-{profile}.yml |
배포 시 |
| 시크릿 | Vault / AWS SSM | EnvironmentPostProcessor |
| 피처 플래그 | DB / Config Server | @RefreshScope |
| 인프라 오버라이드 | 환경변수 / K8s ConfigMap | Pod 재시작 |
마무리
Spring PropertySource는 단순한 설정 파일 로딩을 넘어 외부 소스 통합, 동적 갱신, 우선순위 제어를 가능하게 하는 강력한 추상화입니다. EnvironmentPostProcessor로 부팅 시점에 Vault나 SSM 시크릿을 주입하고, 커스텀 PropertySource로 DB 기반 동적 설정을 구현하면 재배포 없이 설정을 변경할 수 있는 유연한 아키텍처를 만들 수 있습니다.
관련 글로 Spring ConfigurationProperties 심화와 Spring Cloud Config 중앙 설정 관리도 함께 참고하세요.