외부 설정 우선순위
파일·환경 변수·시스템 속성·명령행 인자를 PropertySource로 통합하는 원리와 환경·@Value의 역할, 우선순위 충돌·출처 추적·비밀 정보 주입·타입 변환 실패를 회원 게시판 설정으로 진단합니다.
배포 환경마다 서버 주소와 타임아웃이 다른 것은 정상입니다.
달라지는 값을 JAR에 다시 컴파일하지 않고 바깥에서 넣되, 애플리케이션 코드는 값이 어디서 왔는지 모르게 만드는 것이 외부 설정의 핵심입니다.
Spring의 Environment는 여러 입력을 PropertySource 목록으로 정렬하고 같은 키를 앞쪽 소스에서 먼저 찾습니다.
운영 문제는 대개 값 자체보다 어느 소스가 이겼는지를 놓쳐서 생깁니다.
Environment의 역할
시스템 속성, OS 환경 변수, 구성 데이터, 테스트 속성은 모양이 다르지만 애플리케이션에는 키-값 조회로 보입니다.
업무 서비스가 System.getenv()나 파일 경로를 직접 읽으면 우선순위와 테스트 대체가 흩어집니다.
부트스트랩 경계에서 Environment 또는 바인딩 객체로 읽고 서비스에는 의미 있는 값만 전달합니다.
다음 예제는 실제 Spring PropertySource 검색 규칙을 작은 모델로 관찰합니다.
이름이 같은 board.api.base-url을 두 소스에 넣으면 앞에 추가한 배포 재정의가 이깁니다.
package board.config;
import java.util.Map;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
public final class EnvironmentPriorityDemo {
private EnvironmentPriorityDemo() {
}
public static void main(String[] args) {
var environment = new StandardEnvironment();
environment.getPropertySources().addLast(new MapPropertySource(
"packaged-defaults",
Map.of("board.api.base-url", "https://api.internal")));
environment.getPropertySources().addFirst(new MapPropertySource(
"deployment-override",
Map.of("board.api.base-url", "https://api.production")));
System.out.println(environment.getRequiredProperty(
"board.api.base-url"));
System.out.println(environment.getPropertySources().stream()
.filter(source -> source.containsProperty("board.api.base-url"))
.map(source -> source.getName())
.toList());
}
}https://api.production
[deployment-override, packaged-defaults]값을 출력하는 것만으로는 출처를 알 수 없습니다. 장애 분석에서는 속성 소스 이름과 활성 프로필, 실제 배포 매니페스트를 함께 보되 비밀번호와 토큰 값은 기록하지 않습니다.
속성 우선순위
Boot는 패키징 구성보다 JAR 바깥 구성을, 일반 구성 데이터보다 환경 변수·시스템 속성·명령행 같은 배포 입력을 우선하도록 조합합니다. 테스트 애노테이션과 개발 도구처럼 특정 실행에만 추가되는 소스도 있습니다. 전체 목록의 번호를 암기하기보다 “기본값은 안쪽, 배포 재정의는 바깥, 일회성 실행 인자는 가장 구체적”이라는 방향을 이해하고 현재 버전의 환경을 관찰합니다.
우선순위가 높다고 좋은 소스는 아닙니다. 모든 값을 명령행에 넣으면 프로세스 목록과 셸 기록에 비밀 정보가 남을 수 있고, 무심코 전달한 인자 하나가 버전 관리 파일을 가립니다. 조직은 허용 소스, 비밀 정보 저장 연동, 수정 권한, 롤백 방법을 운영 규칙으로 정합니다.
동적 조회·불변 정책
getProperty(key, type)은 변환 서비스를 통해 숫자·불리언 같은 타입으로 바꿉니다.
getRequiredProperty는 누락을 즉시 드러내지만 여러 키의 관계까지 검증하지는 못합니다.
여러 곳에서 문자열 키를 반복하면 오타가 컴파일 시간에 잡히지 않고 기본값이 제각각 됩니다.
부트스트랩 컴포넌트가 한 번 읽어 불변 레코드를 만들고 서비스에 주입하면 요청 처리 중 값이 갑자기 바뀌지 않습니다. 런타임 새로 고침이 요구되면 어떤 값이 원자적으로 교체되고 진행 중 요청은 어느 스냅샷을 쓰는지 별도 설계를 합니다.
package board.config;
import java.net.URI;
import java.time.Duration;
import org.springframework.core.env.Environment;
public record BoardApiSettings(URI baseUrl, Duration timeout, int poolSize) {
public BoardApiSettings {
if (!baseUrl.getScheme().equals("https")) {
throw new IllegalArgumentException("baseUrl must use https");
}
if (timeout.isNegative() || timeout.isZero()) {
throw new IllegalArgumentException("timeout must be positive");
}
if (poolSize < 1 || poolSize > 64) {
throw new IllegalArgumentException("poolSize must be between 1 and 64");
}
}
public static BoardApiSettings from(Environment environment) {
return new BoardApiSettings(
URI.create(environment.getRequiredProperty("board.api.base-url")),
Duration.parse(environment.getProperty(
"board.api.timeout", "PT2S")),
environment.getProperty("board.api.pool-size", Integer.class, 8));
}
}pool-size=fast면 정수 변환 예외가, base-url=http://...면 도메인 검증 예외가 시작에서 나야 합니다.
요청이 들어온 뒤 첫 사용 시점까지 실패를 미루지 않습니다.
@Value 사용 범위
@Value("${board.banner:회원 게시판}")는 작은 표시용 값이나 라이브러리 어댑터의 단일 스칼라에 편리합니다.
자리표시자 기본값은 콜론 뒤에 쓰며 SpEL과 함께 사용할 수도 있지만, 표현식이 복잡해질수록 설정 계약이 애노테이션 문자열 안에 숨습니다.
동일 접두사 아래 키가 셋 이상이거나 검증·메타데이터가 필요하면 @ConfigurationProperties로 이동합니다.
package board.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class BannerConfiguration {
@Bean
Banner banner(
@Value("${board.banner:회원 게시판}") String text,
@Value("${board.banner-visible:true}") boolean visible
) {
return new Banner(text, visible);
}
public record Banner(String text, boolean visible) {
public String render() {
return visible ? text : "";
}
}
}필드 주입보다 생성자나 빈 메서드 파라미터를 사용하면 의존성이 보이고 순수 단위 테스트가 쉽습니다.
@Value 자리표시자 해석에는 빈 팩토리 후처리기가 관여하므로 애노테이션을 붙인 객체를 new로 만들었다고 값이 채워지지는 않습니다.
설정 파일·가져오기
클래스 경로의 application.properties 또는 YAML은 JAR의 안전한 기본값에 적합합니다.
같은 이름의 외부 파일은 이미지를 다시 만들지 않고 환경별 값을 바꿉니다.
추가 위치와 가져오기를 사용하면 공통 설정과 비밀 정보 마운트를 조합할 수 있지만 선택적 가져오기와 필수 가져오기를 구분해야 합니다.
필수 파일이 없는데 기본값으로 계속 시작하면 잘못된 데이터베이스나 외부 API로 연결할 수 있습니다.
구성 트리처럼 파일 하나를 키 하나로 읽는 방식은 컨테이너 비밀 정보 마운트와 잘 맞습니다. 그러나 끝 줄바꿈, 파일 권한, 교체 시점, 프로세스 재시작 여부를 검증합니다. 값의 소스를 표준화하지 않고 환경 변수와 마운트를 동시에 갱신하면 높은 소스의 오래된 값이 계속 승리합니다.
설정 실패 진단
설정 오류를 “애플리케이션이 안 뜬다”로 묶지 않습니다. 키가 없으면 누락, 문자열을 타입으로 바꾸지 못하면 변환, 객체는 만들어졌지만 범위를 위반하면 검증, 두 소스가 다르면 우선순위 문제입니다.
missing key: board.api.base-url -> REQUIRED_PROPERTY_MISSING
raw value: board.api.pool-size=fast -> TYPE_CONVERSION_FAILED
resolved value: board.api.pool-size=128 -> DOMAIN_RANGE_REJECTED
winning source: commandLineArgs -> STALE_OVERRIDE_WON실패 로그에는 키와 소스를 남기되 비밀 정보 원시 값을 붙이지 않습니다. 구성 변경 전후에는 최종 키 집합과 민감 값을 가린 출처 차이를 배포 산출물로 보관합니다.
연습 문제
회원 게시판의 API URL·타임아웃·풀 크기를 JAR 내부 기본값, 로컬 파일, 환경 변수 세 소스로 공급하세요. 운영 환경 URL만 환경 변수가 이기게 만들고, URL이 HTTP이거나 풀이 64를 넘으면 시작을 실패시키세요. 각 키의 최종 적용 소스를 값 없이 표로 남기세요.
해설 보기
소스별 입력을 한 번에 바꾸지 말고 같은 키 하나를 겹쳐 우선순위를 관찰합니다.
서비스에는 Environment 대신 검증을 마친 불변 설정 객체를 전달합니다.
package board.config;
public final class SafeOriginLog {
private SafeOriginLog() {
}
public static String line(String key, String sourceName) {
if (key.toLowerCase().contains("password")
|| key.toLowerCase().contains("token")) {
return key + "=<redacted> source=" + sourceName;
}
return key + " source=" + sourceName;
}
}비밀 정보 여부를 이름 규칙 하나에만 의존하지 말고 Boot 민감 정보 마스킹과 조직 허용 목록을 함께 적용합니다.