타입 안전 외부 설정
ConfigurationProperties로 중첩 설정과 단위를 안전하게 바인딩하고 검증·YAML 재정의·프로필 활성화를 구분합니다.
외부 설정이 열 개를 넘으면 문자열 키를 개별 조회하는 방식은 구조를 잃습니다.
@ConfigurationProperties는 같은 접두사의 값을 하나의 타입으로 묶고, 생성자 바인딩과 검증으로 애플리케이션이 사용할 수 있는 상태만 허용합니다.
YAML은 사람이 읽는 입력 표현이고 속성 객체는 코드가 의존하는 계약입니다.
두 층을 같은 것으로 취급하지 않습니다.
프로필은 입력 문서를 고르고 바인더는 하나의 유효한 설정 객체를 만든다
프로필 입력·바인딩·변환·선언된 제약 검증·시작 성공 또는 실패의 상태 경계가 있다.
CONFIGURATION PROPERTIES · BIND AND VALIDATE
프로필은 문서를 선택하고 바인더는 타입을 만든다
YAML/properties는 입력 표현이고 ConfigurationProperties는 코드의 설정 계약입니다. 문서 선택·우선순위·타입 변환과 의미 검증을 구분합니다.
입력 → 설정 객체
- 프로필과 소스에서 입력 선택
활성 프로필로 문서를 고르고 같은 키의 우선순위를 적용합니다. 프로필 이름만으로 안전성을 보장하지 않습니다.
- 완화된 바인딩과 타입 변환
정규 batch-size 키, Duration·URI·중첩 객체를 바인딩합니다. 예시 단위는 ms/s/MB처럼 명시합니다.
- 등록과 실제 시작 검증
ConfigurationPropertiesScan 또는 EnableConfigurationProperties로 등록합니다. Validated·검증 공급자와 실제 실패를 확인합니다.
원문이 검사하는 범위
현재: endpoint·timeout 필수, batchSize 1~500, retry가 있으면 maximumAttempts 1~5와 backoff 필수입니다.
추가할 정책: HTTPS, 양수 Duration, retry 필수는 원문이 검사하지 않습니다. @Valid만으로 retry 누락을 거부하지 않습니다.
목록: 높은 소스가 regions 목록 전체를 교체합니다. 한 원소만 바꿔 낮은 목록의 나머지가 병합된다고 가정하지 않습니다.
생성자 테스트와 바인더 테스트는 다른 증거입니다. 프로필별 최종 객체·목록 교체·누락/경계값·중첩 검증을 확인하고 속성의 필수성·단위·민감성·재시작 계약을 문서화합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 설정 등록과 검증 공급자가 준비됐다는 전제다. 변환 성공 후 BoardStorageProperties에 선언된 NotNull/Min/Max와 존재하는 Retry의 Valid를 검사한다. null Retry·HTTP·음수 Duration을 추가 정책 없이 자동 거부하지 않는다. 변환 실패와 제약 실패는 시작 실패의 다른 원인이다. 모바일도 같은 5노드와 5관계이고 실제 바인딩·Spring 시작은 ContextRunner로 별도 확인한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 프로필은 입력 문서를 고르고 바인더는 하나의 유효한 설정 객체를 만든다 |
| 6 | 설정 등록과 검증 공급자가 준비됐다는 전제다. 변환 성공 후 BoardStorageProperties에 선언된 NotNull/Min/Max와 존재하는 Retry의 Valid를 검사한다. null Retry·HTTP·음수 Duration을 추가 정책 없이 자동 거부하지 않는다. 변환 실패와 제약 실패는 시작 실패의 다른 원인이다. 모바일도 같은 5노드와 5관계이고 실제 바인딩·Spring 시작은 ContextRunner로 별도 확인한다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 프로필 입력 바인딩 선택된 문서를 바인더가 타입으로 변환합니다. | 입력: 프로필 문서 · 실패: 변환 실패 | 문자열 자료가 설정 값이 됩니다. |
| 선언된 제약 검사 NotNull·Min·Max와 존재하는 Retry의 Valid를 검사합니다. | 검사: 선언된 제약 · 추가 정책: 자동 추가 없음 | 실제 선언한 정책만 시작 조건이 됩니다. |
| 시작 또는 실패 변환·검증을 통과한 설정으로 시작하거나 원인별로 실패합니다. | 정상: 설정 객체 · 실패: 시작 실패 · 증거: ContextRunner | 유효 설정과 실제 시작 증거를 구분합니다. |
프로필은 입력 문서를 고르고 바인더는 하나의 유효한 설정 객체를 만든다
프로필 입력·바인딩·변환·선언된 제약 검증·시작 성공 또는 실패의 상태 경계가 있다.
CONFIGURATION PROPERTIES · BIND AND VALIDATE
프로필은 문서를 선택하고 바인더는 타입을 만든다
YAML/properties는 입력 표현이고 ConfigurationProperties는 코드의 설정 계약입니다. 문서 선택·우선순위·타입 변환과 의미 검증을 구분합니다.
입력 → 설정 객체
- 프로필과 소스에서 입력 선택
활성 프로필로 문서를 고르고 같은 키의 우선순위를 적용합니다. 프로필 이름만으로 안전성을 보장하지 않습니다.
- 완화된 바인딩과 타입 변환
정규 batch-size 키, Duration·URI·중첩 객체를 바인딩합니다. 예시 단위는 ms/s/MB처럼 명시합니다.
- 등록과 실제 시작 검증
ConfigurationPropertiesScan 또는 EnableConfigurationProperties로 등록합니다. Validated·검증 공급자와 실제 실패를 확인합니다.
원문이 검사하는 범위
현재: endpoint·timeout 필수, batchSize 1~500, retry가 있으면 maximumAttempts 1~5와 backoff 필수입니다.
추가할 정책: HTTPS, 양수 Duration, retry 필수는 원문이 검사하지 않습니다. @Valid만으로 retry 누락을 거부하지 않습니다.
목록: 높은 소스가 regions 목록 전체를 교체합니다. 한 원소만 바꿔 낮은 목록의 나머지가 병합된다고 가정하지 않습니다.
생성자 테스트와 바인더 테스트는 다른 증거입니다. 프로필별 최종 객체·목록 교체·누락/경계값·중첩 검증을 확인하고 속성의 필수성·단위·민감성·재시작 계약을 문서화합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 설정 등록과 검증 공급자가 준비됐다는 전제다. 변환 성공 후 BoardStorageProperties에 선언된 NotNull/Min/Max와 존재하는 Retry의 Valid를 검사한다. null Retry·HTTP·음수 Duration을 추가 정책 없이 자동 거부하지 않는다. 변환 실패와 제약 실패는 시작 실패의 다른 원인이다. 모바일도 같은 5노드와 5관계이고 실제 바인딩·Spring 시작은 ContextRunner로 별도 확인한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 프로필은 입력 문서를 고르고 바인더는 하나의 유효한 설정 객체를 만든다 |
| 6 | 설정 등록과 검증 공급자가 준비됐다는 전제다. 변환 성공 후 BoardStorageProperties에 선언된 NotNull/Min/Max와 존재하는 Retry의 Valid를 검사한다. null Retry·HTTP·음수 Duration을 추가 정책 없이 자동 거부하지 않는다. 변환 실패와 제약 실패는 시작 실패의 다른 원인이다. 모바일도 같은 5노드와 5관계이고 실제 바인딩·Spring 시작은 ContextRunner로 별도 확인한다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 프로필 입력 바인딩 선택된 문서를 바인더가 타입으로 변환합니다. | 입력: 프로필 문서 · 실패: 변환 실패 | 문자열 자료가 설정 값이 됩니다. |
| 선언된 제약 검사 NotNull·Min·Max와 존재하는 Retry의 Valid를 검사합니다. | 검사: 선언된 제약 · 추가 정책: 자동 추가 없음 | 실제 선언한 정책만 시작 조건이 됩니다. |
| 시작 또는 실패 변환·검증을 통과한 설정으로 시작하거나 원인별로 실패합니다. | 정상: 설정 객체 · 실패: 시작 실패 · 증거: ContextRunner | 유효 설정과 실제 시작 증거를 구분합니다. |
불변 설정 객체
회원 게시판 저장소 설정에는 엔드포인트, 타임아웃, 배치 크기, 재시도 정책이 함께 움직입니다.
각각 @Value로 흩어 놓으면 키 이름 변경과 관계 검증이 어렵습니다.
레코드 생성자에 바인딩하면 생성 뒤 값이 바뀌지 않고 필수 컴포넌트가 타입에 나타납니다.
package board.config;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties("board.storage")
public record BoardStorageProperties(
@NotNull URI endpoint,
@NotNull Duration timeout,
@Min(1) @Max(500) int batchSize,
@Valid Retry retry,
List<String> regions
) {
public BoardStorageProperties {
regions = regions == null ? List.of() : List.copyOf(regions);
}
public record Retry(
@Min(1) @Max(5) int maximumAttempts,
@NotNull Duration backoff
) {
}
}단일 생성자인 레코드에는 별도 생성자-바인딩 애노테이션이 필요하지 않습니다.
컴포넌트 스캔과 혼동하지 말고 @ConfigurationPropertiesScan 또는 @EnableConfigurationProperties(BoardStorageProperties.class)로 등록합니다.
라이브러리 자동 구성은 후자를 사용해 공개 설정 타입을 명시하는 편이 분명합니다.
완화된 바인딩
Java 필드 batchSize에는 board.storage.batch-size, 환경 변수 BOARD_STORAGE_BATCHSIZE 같은 표기가 연결됩니다.
이는 배포 시스템의 이름 지정 차이를 흡수하는 규칙이지 서로 다른 별칭을 무제한 허용한다는 뜻이 아닙니다.
문서와 메타데이터에는 정규 케밥 표기법 키 하나를 사용합니다.
Duration에는 2s, 500ms 같은 단위를, 데이터 크기에는 10MB 같은 단위를 붙입니다.
단위 없는 숫자는 코드 기본값 단위에 기대게 되어 운영자가 잘못 읽기 쉽습니다.
URI, 열거형, 목록, 맵처럼 변환 가능한 타입을 사용하되 의미 검증은 생성자나 Jakarta 제약 조건으로 추가합니다.
시작 시 설정 검증
타입 변환 성공은 유효한 설정이라는 뜻이 아닙니다.
배치 크기 0은 정수지만 처리 반복을 깨뜨리고, HTTP 엔드포인트는 URI지만 비밀 정보 전송 정책을 위반할 수 있습니다.
필드 단일 범위는 제약 조건으로, 여러 필드 관계는 생성자 불변식이나 클래스 수준 검증기로 검사합니다.
중첩 객체 검증에는 @Valid를 붙입니다. 이 예제의 retry는 @NotNull이 없어 누락 자체를 거부하지 않으며, URI의 HTTPS 여부와 Duration의 양수 여부도 검사하지 않습니다. 이 정책은 연습 문제에서 추가할 검증입니다.
예시 원 제약만 적용하고 변환이 성공했다고 정하면 endpoint=http://storage.internal·timeout=-1s·batchSize=50·retry 누락·regions 누락은 HTTPS·양수·재시도 필수라는 정책 때문에 거부되지 않습니다. endpoint와 timeout은 null이 아니고 batchSize는 범위 안이며, regions는 생성자에서 빈 목록으로 바뀝니다. Jakarta 객체 그래프 검증은 @Valid의 null 참조를 건너뜁니다. 원하는 보안·시간 정책과 현재 애노테이션이 실제 검사하는 범위를 따로 적으세요.
이 추론은 선언된 제약의 허용 범위입니다. 원 ContextRunner 테스트를 실행해 이 입력의 실제 바인딩·시작 결과를 확인한 기록은 아닙니다.
설정 빈에 @Validated가 없거나 검증 공급자가 빠지면 애노테이션이 장식으로 남을 수 있으므로 유효하지 않은 속성으로 컨텍스트가 실패하는 테스트를 둡니다.
board.storage.batch-size=0
binding target=BoardStorageProperties
field=batchSize
constraint=Min(1)
result=APPLICATION_STARTUP_REJECTED오류 메시지에는 정규 속성 경로와 거부된 이유를 남기고 비밀번호 원시 값은 마스킹합니다.
기본값으로 보정해 시작할 값과 반드시 거부할 값을 설정 정책 표에 나눕니다.
바인더 단위 테스트
설정 타입의 순수 생성자 테스트만으로 느슨한 바인딩과 단위 변환은 검증되지 않습니다.
ApplicationContextRunner에 속성 값을 넣고 속성 빈과 시작 실패를 관찰하면 작은 실제 컨텍스트로 규칙을 고정할 수 있습니다.
package board.config;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
class StoragePropertiesContextTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withUserConfiguration(PropertyConfiguration.class);
@Test
void bindsUnitsAndNestedRetry() {
runner.withPropertyValues(
"board.storage.endpoint=https://storage.internal",
"board.storage.timeout=750ms",
"board.storage.batch-size=50",
"board.storage.retry.maximum-attempts=3",
"board.storage.retry.backoff=100ms",
"board.storage.regions[0]=ap-northeast-2")
.run(context -> {
var properties = context.getBean(BoardStorageProperties.class);
assertThat(properties.timeout().toMillis()).isEqualTo(750);
assertThat(properties.retry().maximumAttempts()).isEqualTo(3);
assertThat(properties.regions()).containsExactly("ap-northeast-2");
});
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(BoardStorageProperties.class)
static class PropertyConfiguration {
}
}유효하지 않은 분기에서는 context.getStartupFailure()와 원인 연쇄의 속성 경로를 검증합니다.
전체 @SpringBootTest 하나로 바인딩 성공만 확인하면 누락 키와 경계값을 놓칩니다.
YAML 평탄화
다음 YAML은 점 표기 속성으로 펼쳐져 같은 바인더에 들어갑니다.
들여쓰기는 구조이므로 탭과 수준 오류를 검토하고, 값에 콜론이나 특수 문자가 있으면 따옴표로 묶습니다.
.properties와 YAML을 동시에 유지하면 어느 파일이 기준인지 불분명하므로 주 형식을 하나로 정합니다.
board:
storage:
endpoint: "https://storage.internal"
timeout: 750ms
batch-size: 50
retry:
maximum-attempts: 3
backoff: 100ms
regions:
- ap-northeast-2
- ap-southeast-1맵은 키별 재정의가 가능한 경우가 있지만 목록은 높은 소스가 전체 목록을 교체한다고 보는 것이 안전합니다.
운영 환경 프로필에서 regions[0] 하나만 바꿔 나머지가 합쳐질 것이라 기대하지 않습니다.
최종 결합된 객체를 테스트하고 프로필별 구성 차이를 검토합니다.
프로필별 설정 선택
프로필은 로컬·테스트·운영 환경 같은 실행 환경에 따라 구성 문서를 활성화합니다.
spring.config.activate.on-profile로 문서가 적용될 조건을 쓰고, 활성 프로필은 명령 라인이나 환경처럼 외부에서 결정합니다.
활성화될 문서 안에서 활성 프로필 자체를 다시 바꾸는 순환 설정은 피합니다.
board:
storage:
endpoint: "https://storage.local"
timeout: 2s
---
spring:
config:
activate:
on-profile: production
board:
storage:
endpoint: "https://storage.production"
timeout: 500ms@Profile로 데이터베이스 어댑터 빈 전체를 교체할 수 있지만 프로필 이름이 도메인 코드 곳곳에 퍼지면 조합 수가 급격히 늘어납니다.
기능이 속성 하나로 결정된다면 조건부 속성이 더 직접적입니다.
프로필 그룹은 production이 observability와 secure-cookie 같은 묶음을 활성화하게 할 수 있지만 실제 활성 프로필 집합을 시작 로그와 테스트에서 확인합니다.
설정 메타데이터·문서
구성 처리기가 생성한 메타데이터는 IDE 자동 완성과 설명 표시를 돕습니다.
사용 중단된 키에는 대체 키와 마이그레이션 기간을 남기고, 알 수 없는 키를 무조건 무시할지 엄격히 거부할지 결정합니다.
공개 스타터는 접두사, 기본값, 필수 여부, 단위, 민감성, 재시작 필요 여부를 버전이 있는 API처럼 관리합니다.
운영에서는 전체 구성 덤프 대신 허용 목록에 등록된 비민감 값과 출처를 보여 줍니다.
프로필 이름만으로 운영 환경 안전성을 가정하지 않고 실제 결합된 설정값을 마스킹된 스냅샷으로 비교합니다.
연습 문제
회원 게시판의 내보내기 엔드포인트, 연결/읽기 타임아웃, 배치 크기, 재시도, 영역 목록을 불변 속성으로 만드세요.
로컬과 운영 환경 YAML 문서를 작성하고 운영 환경에서 HTTP 엔드포인트, 배치 0, 재시도 6이 각각 시작을 막는 테스트를 추가하세요.
목록이 병합된다고 가정하지 말고 최종 regions를 검증하세요.
해설 보기
제약 조건 애노테이션만 나열하기 전에 설정 표에서 허용 범위와 기본값의 근거를 정합니다.
여러 값을 함께 보는 정책은 레코드 생성자에 둡니다.
package board.config;
import java.time.Duration;
public record TimeoutPolicy(Duration connect, Duration read) {
public TimeoutPolicy {
if (connect.isNegative() || connect.isZero()) {
throw new IllegalArgumentException("connect timeout must be positive");
}
if (read.compareTo(connect) < 0) {
throw new IllegalArgumentException("read timeout must not be shorter than connect timeout");
}
}
}운영자가 단위를 빠뜨리지 않도록 문서 예시는 항상 ms, s, MB를 명시합니다.
독립 확인: 변환·설정 등록·검증 공급자가 준비된 조건입니다. A는 batchSize=0·retry 없음, B는 batchSize=50·retry.maximumAttempts=6·backoff=100ms, C는 batchSize=50·retry 없음입니다. 원 제약에서 각각 어느 부분이 거부되나요? TimeoutPolicy(connect=100ms, read=50ms)는 어떤 별도 관계를 검사하나요?
풀이 확인
A는 batchSize의 Min(1), B는 존재하는 Retry에 연쇄 검증한 Max(5)에서 거부됩니다. C는 retry의 null을 @Valid가 건너뛰므로 이 누락만으로 거부되지 않습니다. 재시도를 필수로 만들려면 retry에 NotNull 같은 계약을 추가합니다. TimeoutPolicy는 read가 connect보다 짧다는 두 필드 관계를 생성자에서 거부합니다. 서로 타입이 Duration이라는 사실만으로 이 대소 관계가 보장되지는 않습니다.