외부 설정 우선순위
파일·환경 변수·시스템 속성·명령행 인자가 하나의 설정으로 합쳐지는 우선순위와 출처·변환 실패를 진단합니다.
배포 환경마다 서버 주소와 타임아웃이 다른 것은 자연스러운 일입니다. 외부 설정(externalized configuration)의 핵심은 달라지는 값을 JAR을 다시 컴파일하지 않고 바깥에서 넣되, 애플리케이션 코드는 그 값이 어디서 왔는지 몰라도 되게 만드는 것입니다.
Spring의 Environment는 여러 입력을 PropertySource 목록으로 정렬하고, 같은 키를 목록의 앞쪽 소스부터 찾습니다. 운영에서 생기는 설정 문제는 대개 값 자체보다 어느 소스의 값이 이겼는지를 놓쳐서 생깁니다. 이 글에서는 우선순위가 정해지는 방식, 값을 읽어 불변 객체로 만드는 방법, @Value의 쓰임새, 설정 파일과 가져오기, 실패의 종류를 차례로 다룹니다.
같은 키는 출처 우선순위와 검증을 거쳐 하나의 불변 설정이 된다
키별 우선 출처 선택·URI/Duration/Integer 변환·검증·불변 객체로 값이 변하고 실패 시 낮은 값으로 재선택하지 않는다.
EXTERNAL CONFIG · SOURCE THEN CONTRACT
같은 키는 먼저 찾은 출처의 값을 사용한다
Environment는 정렬된 PropertySource를 검색합니다. 부트스트랩에서 승리한 값을 타입으로 변환하고 검증한 불변 정책을 서비스에 전달합니다.
출처 선택 · 같은 키의 경쟁
- 일반 배포 입력의 우선 방향
구성 데이터보다 환경 변수·시스템 속성·명령행이 우선할 수 있습니다. 내부보다 외부 구성 데이터를 우선합니다.
- 실제 검색 목록의 앞쪽이 승리
예제는 deployment override를 기본값 앞에 추가합니다. 현재 Environment 목록과 최종 출처를 관찰합니다.
- 값과 출처를 분리
진단에는 키·source·활성 프로필을 남깁니다. 원시 비밀 값은 출력하지 않습니다.
변환과 검증 · 서비스 계약
- 누락을 부트스트랩에서 확인
필수 키는 getRequiredProperty 등으로 확인합니다. 필수 import와 optional import를 구분합니다.
- 타입 변환 뒤 도메인 검증
pool-size=fast는 변환 실패, 허용하지 않은 HTTP URL은 정책 검증 실패입니다.
- 불변 객체로 서비스에 전달
업무 코드가 파일·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를 검색합니다. 부트스트랩에서 승리한 값을 타입으로 변환하고 검증한 불변 정책을 서비스에 전달합니다.
출처 선택 · 같은 키의 경쟁
- 일반 배포 입력의 우선 방향
구성 데이터보다 환경 변수·시스템 속성·명령행이 우선할 수 있습니다. 내부보다 외부 구성 데이터를 우선합니다.
- 실제 검색 목록의 앞쪽이 승리
예제는 deployment override를 기본값 앞에 추가합니다. 현재 Environment 목록과 최종 출처를 관찰합니다.
- 값과 출처를 분리
진단에는 키·source·활성 프로필을 남깁니다. 원시 비밀 값은 출력하지 않습니다.
변환과 검증 · 서비스 계약
- 누락을 부트스트랩에서 확인
필수 키는 getRequiredProperty 등으로 확인합니다. 필수 import와 optional import를 구분합니다.
- 타입 변환 뒤 도메인 검증
pool-size=fast는 변환 실패, 허용하지 않은 HTTP URL은 정책 검증 실패입니다.
- 불변 객체로 서비스에 전달
업무 코드가 파일·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 설정으로 검증됩니다. |
| 불변 설정 결과 통과하면 불변 설정이고 실패 때 낮은 값을 다시 고르지 않습니다. | 정상: 불변 객체 · 실패: 재선택 없음 | 우선순위 선택과 유효성 검사를 분리합니다. |
그림의 위쪽은 이 글의 연습 문제에서 쓸 조건으로, 높은 소스에는 URL만 있고 낮은 소스에는 URL, 시간, 풀 크기가 모두 있을 때 키마다 처음 발견한 값을 고르는 흐름입니다. URL은 높은 소스에서, 시간과 풀 크기는 낮은 소스에서 오며, 고른 값을 URI, Duration, Integer로 바꾸고 HTTPS, 양수, 1~64 범위를 검사합니다. 하나라도 실패하면 시작이 실패하고, 낮은 소스의 값으로 다시 돌아가 고르지 않습니다. 아래 칸들은 출처를 고르는 방향과, 변환·검증을 거쳐 불변 객체로 서비스에 넘기는 과정을 정리하며, 이 그림이 Boot의 전체 소스 우선순위 표를 대신하지는 않습니다.
Environment의 역할
시스템 속성, 운영체제 환경 변수, 구성 데이터, 테스트 속성은 모양이 서로 다르지만, 애플리케이션에는 모두 키로 값을 조회하는 대상으로 보입니다. 업무 서비스가 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());
}
}첫 줄은 이긴 값이고, 둘째 줄은 이 키를 가진 소스의 이름을 검색 순서대로 나열한 것입니다. 둘째 줄은 getPropertySources()를 앞에서부터 돌며 키를 가진 소스만 남긴 목록이므로, 값을 고를 때 거치는 순서와 같습니다. getRequiredProperty는 키를 찾지 못하면 null 대신 IllegalStateException을 던지므로, 이 값이 필수라는 사실도 코드에 드러납니다.
https://api.production
[deployment-override, packaged-defaults]StandardEnvironment에는 처음부터 시스템 속성과 환경 변수 소스가 들어 있으므로, 위 출력은 그 두 소스에 이 키가 없다는 조건에서 읽습니다. deployment-override는 addFirst로 넣어 가장 앞에 오고, packaged-defaults는 addLast로 넣어 가장 뒤에 옵니다. 우선순위는 소스 하나를 통째로 고르는 규칙이 아니라, 키마다 처음 발견한 값을 고르는 규칙입니다.
값만 출력해서는 그 값이 어디서 왔는지 알 수 없습니다. 장애를 분석할 때는 속성 소스의 이름, 활성 프로필, 실제 배포 매니페스트를 함께 보되, 비밀번호와 토큰의 값은 기록하지 않습니다.
속성의 우선순위
Boot는 JAR 안에 패키징된 구성보다 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://...라면 도메인 검증 예외가 시작할 때 나야 합니다. 요청이 들어온 뒤 처음 쓰는 시점까지 실패를 미루지 않습니다.
이 코드는 Boot의 바인더가 아니라 Duration.parse의 ISO-8601 형식을 직접 씁니다. 그래서 PT2S는 파싱하지만 2s는 실패합니다. baseUrl도 스킴이 있는 URI를 전제로 합니다. /api 같은 상대 URI는 getScheme()이 null이라 equals 호출에서 NullPointerException이 납니다. 시작 실패를 의도한 도메인 메시지로 정리하려면 null, 스킴, 호스트에 대한 정책을 명시해야 하며, 스킴 검사만으로 실제 서버가 있는지나 접근할 수 있는지까지 확인되지는 않습니다.
@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 안에 담는 안전한 기본값에 알맞습니다. 같은 이름의 외부 파일을 두면 이미지를 다시 만들지 않고 환경별 값을 바꿀 수 있습니다.
추가 위치와 가져오기(spring.config.import)를 쓰면 공통 설정과 비밀 정보 마운트를 조합할 수 있지만, 선택적 가져오기와 필수 가져오기를 구분해야 합니다. 필수 파일이 없는데 기본값으로 계속 시작하면, 잘못된 데이터베이스나 외부 API에 연결할 수 있습니다.
구성 트리(configtree)처럼 파일 하나를 키 하나로 읽는 방식은 컨테이너의 비밀 정보 마운트와 잘 맞습니다. 다만 파일 끝의 줄바꿈, 파일 권한, 교체 시점, 프로세스 재시작 필요 여부를 검증합니다. 값의 소스를 하나로 정하지 않고 환경 변수와 마운트를 함께 갱신하면, 우선순위가 높은 소스에 남은 오래된 값이 계속 이깁니다.
더 깊이: 가져온 파일이 가져온 쪽을 이기는 이유
Spring Boot 문서의 외부 설정은 spring.config.import로 가져온 파일을, 가져오기를 선언한 문서 바로 아래에 끼워 넣은 추가 문서로 다룬다고 설명합니다. 그 결과 가져온 파일의 값이 가져오기를 선언한 파일의 값보다 우선합니다. 여러 위치를 가져오면 적은 순서대로 처리되고, 뒤에 가져온 것이 앞의 것을 이깁니다. 그래서 application.properties에 기본값을 두고 비밀 정보 파일을 가져오면 가져온 파일의 값이 쓰이지만, 반대로 공통 설정 파일을 가져오면서 같은 키를 원래 파일에 적어 두었다면 원래 파일의 값이 가려집니다.
가져올 위치가 없으면 기본으로 ConfigDataLocationNotFoundException이 나고 애플리케이션이 시작하지 않습니다. 없어도 되는 위치에만 optional: 접두사를 붙이는 것이, 앞에서 말한 필수 가져오기와 선택적 가져오기를 구분하는 실제 방법입니다. 같은 문서는 파일 자체의 순서도 정해 둡니다. JAR 안의 application 파일, JAR 안의 프로필별 파일, JAR 밖의 application 파일, JAR 밖의 프로필별 파일 순으로 뒤의 것이 이기며, 같은 위치에 .properties와 YAML이 함께 있으면 .properties가 우선합니다. 또 @PropertySource로 추가한 소스는 컨텍스트를 새로 고칠 때에야 Environment에 들어가므로, 그보다 먼저 읽히는 logging.*이나 spring.main.* 같은 속성은 이 방법으로 바꿀 수 없습니다.
설정 실패 진단
설정 오류를 애플리케이션이 안 뜬다는 한마디로 묶지 않습니다. 키가 없으면 누락, 문자열을 타입으로 바꾸지 못하면 변환 실패, 객체는 만들어졌지만 범위를 어기면 검증 실패, 두 소스의 값이 다르면 우선순위 문제입니다.
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실패 로그에는 키와 소스를 남기되, 비밀 정보의 원래 값은 붙이지 않습니다. 구성을 바꾸기 전과 후에는 최종 키 집합과, 민감한 값을 가린 출처의 차이를 배포 산출물로 보관합니다.
정리
Environment는 여러PropertySource를 정렬해 두고, 같은 키를 앞쪽 소스부터 찾아 처음 발견한 값을 씁니다.- 배포 입력과 JAR 바깥의 구성이 패키징된 기본값보다 우선하지만, 우선순위가 높은 소스가 더 안전한 소스라는 뜻은 아닙니다.
- 부트스트랩 단계에서 값을 읽어 변환하고 검증한 불변 객체를 서비스에 넘기며, 실패는 시작 시점에 드러냅니다.
- 설정 실패는 누락, 변환, 검증, 우선순위 문제로 나눠 진단하고, 로그에는 키와 출처만 남깁니다.
연습 문제
1. 세 소스로 공급하는 설정
회원 게시판의 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의 민감 정보 마스킹과 조직의 허용 목록을 함께 적용합니다.
2. 키마다 다른 출처
상속된 시스템 소스에는 이 키들이 없다고 합니다. 높은 소스에는 board.api.base-url=https://api.production만 있고, 낮은 소스에는 board.api.base-url=https://api.internal, board.api.timeout=PT0.75S, board.api.pool-size=8이 있습니다. BoardApiSettings.from이 만드는 세 값과 각 값의 출처를 구하세요. 별도로 높은 소스에 board.api.pool-size=128을 추가하면 낮은 소스의 8로 되돌아가나요?
해설 보기
URL은 높은 소스의 https://api.production, 타임아웃 750 ms와 풀 크기 8은 낮은 소스에서 옵니다. 키마다 높은 소스에 값이 없을 때만 다음 소스로 넘어갑니다.
높은 소스의 128은 정수 변환에는 성공하지만 1~64 범위 검사에서 실패하며, 낮은 소스의 8을 다시 조회하지 않습니다. 높은 소스에 값이 없는 경우, 값이 변환에 실패하는 경우, 범위 검사에 실패하는 경우를 구별해야 합니다.
3. 형식이 다른 값
BoardApiSettings.from에 다음 네 입력을 각각 넣으면 어떻게 되나요? 다른 키는 유효한 값입니다. board.api.timeout=2s, board.api.timeout=PT2S, board.api.base-url=/api, board.api.base-url=http://api.internal입니다.
해설 보기
2s는 ISO-8601 형식이 아니어서 Duration.parse가 DateTimeParseException을 던지고, PT2S는 2초로 파싱됩니다. Boot의 @ConfigurationProperties 바인더라면 2s 같은 간단한 표기도 받아들이지만, 이 코드는 Duration.parse를 직접 쓰므로 그 규칙이 적용되지 않습니다.
/api는 스킴이 없어 getScheme()이 null이고, equals 호출에서 NullPointerException이 납니다. http://api.internal은 스킴이 http라서 의도한 IllegalArgumentException("baseUrl must use https")가 납니다. 같은 시작 실패라도 앞의 것은 정책을 명시하지 않아 생긴 예외이므로, null 검사를 더해 같은 도메인 메시지로 정리하는 편이 진단하기 쉽습니다.
다음 글 타입 안전 외부 설정에서는 문자열 키를 여러 곳에서 읽는 대신 @ConfigurationProperties로 접두사 아래의 값을 한 객체에 바인딩하고 검증하는 방법을 다룹니다.