본문 바로가기

외부 설정 우선순위

파일·환경 변수·시스템 속성·명령행 인자가 하나의 설정으로 합쳐지는 우선순위와 출처·변환 실패를 진단합니다.

배포 환경마다 서버 주소와 타임아웃이 다른 것은 정상입니다.

달라지는 값을 JAR에 다시 컴파일하지 않고 바깥에서 넣되, 애플리케이션 코드는 값이 어디서 왔는지 모르게 만드는 것이 외부 설정의 핵심입니다.

Spring의 Environment는 여러 입력을 PropertySource 목록으로 정렬하고 같은 키를 앞쪽 소스에서 먼저 찾습니다.

운영 문제는 대개 값 자체보다 어느 소스가 이겼는지를 놓쳐서 생깁니다.

같은 키는 출처 우선순위와 검증을 거쳐 하나의 불변 설정이 된다

키별 우선 출처 선택·URI/Duration/Integer 변환·검증·불변 객체로 값이 변하고 실패 시 낮은 값으로 재선택하지 않는다.

EXTERNAL CONFIG · SOURCE THEN CONTRACT

같은 키는 먼저 찾은 출처의 값을 사용한다

Environment는 정렬된 PropertySource를 검색합니다. 부트스트랩에서 승리한 값을 타입으로 변환하고 검증한 불변 정책을 서비스에 전달합니다.

값이 없는 경우와 잘못된 값의 다른 경로상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.높은 소스URL만 있음낮은 소스URL·시간·풀키별 선택첫 존재 값변환·검증모두 성공?불변 설정서비스에 주입시작 실패낮은 값으로재선택하지 않음예아니요
값이 없는 경우와 잘못된 값의 다른 경로상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.높은 소스URL만 있음낮은 소스URL·시간·풀키별 선택첫 존재 값변환·검증모두 성공?불변 설정서비스에 주입시작 실패낮은 값으로재선택하지 않음예아니요

출처 선택 · 같은 키의 경쟁

  1. 일반 배포 입력의 우선 방향

    구성 데이터보다 환경 변수·시스템 속성·명령행이 우선할 수 있습니다. 내부보다 외부 구성 데이터를 우선합니다.

  2. 실제 검색 목록의 앞쪽이 승리

    예제는 deployment override를 기본값 앞에 추가합니다. 현재 Environment 목록과 최종 출처를 관찰합니다.

  3. 값과 출처를 분리

    진단에는 키·source·활성 프로필을 남깁니다. 원시 비밀 값은 출력하지 않습니다.

변환과 검증 · 서비스 계약

  1. 누락을 부트스트랩에서 확인

    필수 키는 getRequiredProperty 등으로 확인합니다. 필수 import와 optional import를 구분합니다.

  2. 타입 변환 뒤 도메인 검증

    pool-size=fast는 변환 실패, 허용하지 않은 HTTP URL은 정책 검증 실패입니다.

  3. 불변 객체로 서비스에 전달

    업무 코드가 파일·System.getenv를 직접 읽지 않게 합니다. 런타임 갱신은 별도 원자적 snapshot 설계입니다.

테스트·개발 도구와 사용자 정의 소스는 추가 우선순위를 가질 수 있습니다. 높은 출처가 안전한 값이라는 뜻은 아니므로 허용 소스와 승리한 출처를 함께 검토합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별로 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드와 5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1같은 키는 출처 우선순위와 검증을 거쳐 하나의 불변 설정이 된다
6상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별로 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드와 5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
키별 출처 선택

URL은 높은 소스에서, 시간·풀 값은 낮은 소스에서 고릅니다.

URL: 높은 source · 시간·pool: 낮은 source각 키의 유효 입력이 정해집니다.
타입 변환·검증

URI·Duration·Integer로 바꾸고 HTTPS·양수·1~64를 검사합니다.

변환: URI·Duration·Integer · 실패: 시작 실패선택한 값이 typed 설정으로 검증됩니다.
불변 설정 결과

통과하면 불변 설정이고 실패 때 낮은 값을 다시 고르지 않습니다.

정상: 불변 객체 · 실패: 재선택 없음우선순위 선택과 유효성 검사를 분리합니다.

같은 키는 출처 우선순위와 검증을 거쳐 하나의 불변 설정이 된다

키별 우선 출처 선택·URI/Duration/Integer 변환·검증·불변 객체로 값이 변하고 실패 시 낮은 값으로 재선택하지 않는다.

EXTERNAL CONFIG · SOURCE THEN CONTRACT

같은 키는 먼저 찾은 출처의 값을 사용한다

Environment는 정렬된 PropertySource를 검색합니다. 부트스트랩에서 승리한 값을 타입으로 변환하고 검증한 불변 정책을 서비스에 전달합니다.

값이 없는 경우와 잘못된 값의 다른 경로상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.높은 소스URL만 있음낮은 소스URL·시간·풀키별 선택첫 존재 값변환·검증모두 성공?불변 설정서비스에 주입시작 실패낮은 값으로재선택하지 않음예아니요
값이 없는 경우와 잘못된 값의 다른 경로상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.높은 소스URL만 있음낮은 소스URL·시간·풀키별 선택첫 존재 값변환·검증모두 성공?불변 설정서비스에 주입시작 실패낮은 값으로재선택하지 않음예아니요

출처 선택 · 같은 키의 경쟁

  1. 일반 배포 입력의 우선 방향

    구성 데이터보다 환경 변수·시스템 속성·명령행이 우선할 수 있습니다. 내부보다 외부 구성 데이터를 우선합니다.

  2. 실제 검색 목록의 앞쪽이 승리

    예제는 deployment override를 기본값 앞에 추가합니다. 현재 Environment 목록과 최종 출처를 관찰합니다.

  3. 값과 출처를 분리

    진단에는 키·source·활성 프로필을 남깁니다. 원시 비밀 값은 출력하지 않습니다.

변환과 검증 · 서비스 계약

  1. 누락을 부트스트랩에서 확인

    필수 키는 getRequiredProperty 등으로 확인합니다. 필수 import와 optional import를 구분합니다.

  2. 타입 변환 뒤 도메인 검증

    pool-size=fast는 변환 실패, 허용하지 않은 HTTP URL은 정책 검증 실패입니다.

  3. 불변 객체로 서비스에 전달

    업무 코드가 파일·System.getenv를 직접 읽지 않게 합니다. 런타임 갱신은 별도 원자적 snapshot 설계입니다.

테스트·개발 도구와 사용자 정의 소스는 추가 우선순위를 가질 수 있습니다. 높은 출처가 안전한 값이라는 뜻은 아니므로 허용 소스와 승리한 출처를 함께 검토합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별로 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드와 5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1같은 키는 출처 우선순위와 검증을 거쳐 하나의 불변 설정이 된다
6상속된 시스템 소스에 해당 키가 없고 두 MapPropertySource만 값을 주었다는 별도 연습 조건이다. URL은 높은 소스, 시간·풀은 낮은 소스에서 키별로 선택한다. 원 URI/Duration/Integer 변환과 HTTPS·양수·1~64 검증에 실패하면 시작 실패이며 낮은 값을 다시 선택하지 않는다. 같은 6노드와 5관계가 모바일에도 적용된다. 전체 Boot 소스 우선순위 표를 이 그림으로 대체하지 않는다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
키별 출처 선택

URL은 높은 소스에서, 시간·풀 값은 낮은 소스에서 고릅니다.

URL: 높은 source · 시간·pool: 낮은 source각 키의 유효 입력이 정해집니다.
타입 변환·검증

URI·Duration·Integer로 바꾸고 HTTPS·양수·1~64를 검사합니다.

변환: URI·Duration·Integer · 실패: 시작 실패선택한 값이 typed 설정으로 검증됩니다.
불변 설정 결과

통과하면 불변 설정이고 실패 때 낮은 값을 다시 고르지 않습니다.

정상: 불변 객체 · 실패: 재선택 없음우선순위 선택과 유효성 검사를 분리합니다.

Environment의 역할

시스템 속성, OS 환경 변수, 구성 데이터, 테스트 속성은 모양이 다르지만 애플리케이션에는 키-값 조회로 보입니다.

업무 서비스가 System.getenv()나 파일 경로를 직접 읽으면 우선순위와 테스트 대체가 흩어집니다.

부트스트랩 경계에서 Environment 또는 바인딩 객체로 읽고 서비스에는 의미 있는 값만 전달합니다.

다음 예제는 실제 Spring PropertySource 검색 규칙을 작은 모델로 관찰합니다.

이름이 같은 board.api.base-url을 두 소스에 넣으면 앞에 추가한 배포 재정의가 이깁니다.

src/main/java/board/config/EnvironmentPriorityDemo.java
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());
    }
}
우선순위와 origin 관찰
https://api.production
[deployment-override, packaged-defaults]

값을 출력하는 것만으로는 출처를 알 수 없습니다.

원 StandardEnvironment에는 시스템 속성과 환경 변수 소스도 처음부터 있습니다. 위 두 줄 출력은 해당 키가 그 소스들에 없다는 조건에서 읽습니다. deployment-override는 addFirst라 가장 앞에 오지만 뒤에 출력되는 출처 목록은 실제 같은 키를 가진 모든 소스를 포함합니다. 우선순위는 소스 하나를 통째로 고르는 규칙이 아니라 각 키에서 처음 발견한 값을 고르는 규칙입니다.

장애 분석에서는 속성 소스 이름과 활성 프로필, 실제 배포 매니페스트를 함께 보되 비밀번호와 토큰 값은 기록하지 않습니다.


속성 우선순위

Boot는 패키징 구성보다 JAR 바깥 구성을, 일반 구성 데이터보다 환경 변수·시스템 속성·명령행 같은 배포 입력을 우선하도록 조합합니다.

테스트 애노테이션과 개발 도구처럼 특정 실행에만 추가되는 소스도 있습니다.

전체 목록의 번호를 암기하기보다 “기본값은 안쪽, 배포 재정의는 바깥, 일회성 실행 인자는 가장 구체적”이라는 방향을 이해하고 현재 버전의 환경을 관찰합니다.

우선순위가 높다고 좋은 소스는 아닙니다.

모든 값을 명령행에 넣으면 프로세스 목록과 셸 기록에 비밀 정보가 남을 수 있고, 무심코 전달한 인자 하나가 버전 관리 파일을 가립니다.

조직은 허용 소스, 비밀 정보 저장 연동, 수정 권한, 롤백 방법을 운영 규칙으로 정합니다.


동적 조회·불변 정책

getProperty(key, type)은 변환 서비스를 통해 숫자·불리언 같은 타입으로 바꿉니다.

getRequiredProperty는 누락을 즉시 드러내지만 여러 키의 관계까지 검증하지는 못합니다.

여러 곳에서 문자열 키를 반복하면 오타가 컴파일 시간에 잡히지 않고 기본값이 제각각 됩니다.

부트스트랩 컴포넌트가 한 번 읽어 불변 레코드를 만들고 서비스에 주입하면 요청 처리 중 값이 갑자기 바뀌지 않습니다.

런타임 새로 고침이 요구되면 어떤 값이 원자적으로 교체되고 진행 중 요청은 어느 스냅샷을 쓰는지 별도 설계를 합니다.

src/main/java/board/config/BoardApiSettings.java
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://...면 도메인 검증 예외가 시작에서 나야 합니다.

요청이 들어온 뒤 첫 사용 시점까지 실패를 미루지 않습니다.

여기서는 Boot 바인더가 아니라 Duration.parse의 ISO 형식을 직접 사용합니다. PT2S는 파싱하지만 2s는 이 코드에서 실패합니다. baseUrl도 스킴이 있는 URI를 전제로 합니다. /api 같은 상대 URI는 getScheme이 null이어서 원 equals에서 NullPointerException이 납니다. 시작 실패를 의도한 도메인 메시지로 정리하려면 null·스킴·호스트 정책을 명시해야 하며, 현재 스킴 검사만으로 실제 서버의 존재나 접근 가능성까지 확인되지는 않습니다.


@Value 사용 범위

@Value("${board.banner:회원 게시판}")는 작은 표시용 값이나 라이브러리 어댑터의 단일 스칼라에 편리합니다.

자리표시자 기본값은 콜론 뒤에 쓰며 SpEL과 함께 사용할 수도 있지만, 표현식이 복잡해질수록 설정 계약이 애노테이션 문자열 안에 숨습니다.

동일 접두사 아래 키가 셋 이상이거나 검증·메타데이터가 필요하면 @ConfigurationProperties로 이동합니다.

src/main/java/board/config/BannerConfiguration.java
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 민감 정보 마스킹과 조직 허용 목록을 함께 적용합니다.

독립 확인: 상속된 시스템 소스에 이 키들이 없다고 정합니다. 높은 소스에는 base-url=https://api.production만 있고 낮은 소스에는 base-url=https://api.internal·timeout=PT0.75S·pool-size=8이 있습니다. 원 BoardApiSettings.from의 세 값과 최종 출처를 구하세요. 별도로 높은 소스에 pool-size=128을 추가하면 낮은 8로 되돌아가나요?

풀이 확인

URL은 높은 소스, 타임아웃 750 ms와 풀 8은 낮은 소스입니다. 각 키에서 높은 소스에 값이 없을 때만 다음으로 갑니다. 높은 128은 정수 변환에 성공한 뒤 1~64 범위 검사에서 실패하며 낮은 8로 재조회하지 않습니다. 높은 값의 부재·변환 실패·범위 실패를 구별해야 합니다.