빌드노트
Spring Boot 프로필에 서버 하나를 적으면 기본 List가 교체된다
Spring Boot 프로필에 List 한 항목을 적으면 기본 목록 전체가 교체된다. 기본·dev·Map만 추가·완성 목록의 실제 바인딩을 비교하고, List로 전체 집합을 지정할지 Map으로 이름별 속성을 유지·수정할지 고른다.
@ConfigurationProperties로 받는 서버 목록에 기본 주소 두 개가 있고, application-dev.yaml에 주소 하나를 적었다. 세 개가 될 것 같지만 바인딩 결과는 하나일 수 있다. Spring Boot는 여러 설정 출처의 List를 이어 붙이지 않고, 우선순위가 높은 쪽의 목록 전체를 사용한다. 기본 항목을 유지하며 이름별로 추가·수정하려면 Map 형태의 설정 계약이 맞는지 검토한다.
여기서 기본값은 application.yaml에 적은 값이다. Java 필드에 서버 객체를 미리 채워 둔 경우와 구분한다. 아래 결과는 이 동작을 확인하려고 새로 만든 최소 Boot 앱에서 얻었다.
주입된 목록의 개수와 값을 함께 본다
먼저 서버를 사용하는 코드에 주입된 설정 빈을 확인한다. 예제는 @ConfigurationProperties("cluster")로 등록한 JavaBean에 List<Server> servers와 Map<String, Server> byName이 있고, 각각 getter/setter를 제공하는 구성이다. Server에는 문자열 url, note와 getter/setter가 있으며 note의 초기값은 null이다. 두 컬렉션은 처음에 비어 있다.
다음 진단은 활성 프로필과 바인딩된 항목을 함께 확인한다. 기존 소비 코드의 props는 주입된 설정 빈, environment는 주입된 org.springframework.core.env.Environment다. 자기 앱에서는 빈과 getter 이름을 맞춘다.
System.out.println("active=" + java.util.Arrays.toString(environment.getActiveProfiles()));
System.out.println("list.count=" + props.getServers().size());
props.getServers().forEach(s ->
System.out.println("list=" + s.getUrl() + "|" + s.getNote()));
System.out.println("map.count=" + props.getByName().size());
new java.util.TreeMap<>(props.getByName()).forEach((key, s) ->
System.out.println("map[" + key + "]=" + s.getUrl() + "|" + s.getNote()));
파일에서 주소를 찾는 것만으로는 실제 소비자가 받는 목록을 알 수 없다. 이 코드는 바인딩이 끝난 빈을 읽는다. Map은 값 비교가 쉽도록 키 순으로 정렬해 출력했다.
같은 프로필에서 List는 하나, Map은 세 개였다
두 자료형의 차이를 비교하려고 별도 config 폴더의 application.yaml에 같은 서버 두 개를 넣었다. servers는 순서가 있는 목록이고, by-name은 a, b라는 이름으로 서버를 구분하는 맵이다. 주소는 합성 문자열이며 연결하지 않았다.
cluster:
servers:
- url: https://a.example.invalid
note: base-a
- url: https://b.example.invalid
note: base-b
by-name:
a:
url: https://a.example.invalid
note: base-a
b:
url: https://b.example.invalid
note: base-b
같은 폴더의 application-dev.yaml에는 서버 c를 넣었다. Map 쪽은 기존 키의 일부 속성도 이어받는지 확인하려고 a의 URL만 추가로 바꿨다.
cluster:
servers:
- url: https://c.example.invalid
by-name:
a:
url: https://a-dev.example.invalid
c:
url: https://c.example.invalid
JDK 17로 실행할 수 있는 로컬 최소 Boot 앱에 앞의 설정 빈과 진단을 넣고 패키징한 상태라면 다음처럼 비교한다. demo.jar는 자기 실행 JAR 이름으로 바꾼다. PowerShell에서 JAR와 config 폴더가 있는 디렉터리를 기준으로 실행하며, 다른 프로필이나 같은 키를 주는 외부 입력이 없는 조건이다. 이번 최소 앱은 웹 서버 없이 값을 출력하고 종료한다.
java -jar .\demo.jar --spring.config.location=file:./config/
java -jar .\demo.jar --spring.config.location=file:./config/ --spring.profiles.active=dev
spring.config.location은 기본 설정 검색 위치를 교체한다. 따라서 이 비교는 지정한 폴더의 파일만 읽도록 했다. 디렉터리 위치의 마지막 /도 포함한다. 같은 위치의 프로필 파일은 기본 파일보다 우선한다. 이 조건은 Spring Boot의 설정 파일 위치와 프로필 규칙 (새 탭에서 열림)에 설명돼 있다.
위 두 명령도 격리된 PowerShell에서 실제 실행했다. 출력 중 활성 프로필과 컬렉션에 해당하는 행은 다음과 같았다. 첫 묶음은 프로필 미지정, 두 번째는 dev 실행이다.
active=[]
list.count=2
list=https://a.example.invalid|base-a
list=https://b.example.invalid|base-b
map.count=2
map[a]=https://a.example.invalid|base-a
map[b]=https://b.example.invalid|base-b
active=[dev]
list.count=1
list=https://c.example.invalid|null
map.count=3
map[a]=https://a-dev.example.invalid|base-a
map[b]=https://b.example.invalid|base-b
map[c]=https://c.example.invalid|null
List에는 c 하나만 남았다. 프로필 파일의 첫 항목을 기본 목록 뒤에 붙이지 않았다. 그 항목에 적지 않은 note도 기본 목록 첫 항목의 base-a를 물려받지 않고 null이었다.
Map은 a, b를 유지하며 c를 추가했다. 특히 a는 URL이 프로필 값으로 바뀌었지만 note는 기본 파일의 base-a가 남았다. Map 값이 객체라면 같은 키의 객체 전체를 통째로 교체한다고 생각해서도 안 된다. 여기서는 cluster.by-name.a.url은 프로필에서, cluster.by-name.a.note는 기본 파일에서 가져왔다.
공식 병합 규칙 (새 탭에서 열림)은 List 전체 교체와 Map 속성의 여러 출처 바인딩을 구분한다. 실행 버전 3.5.7의 공식 문서 원본 (새 탭에서 열림)에서도 같은 규칙과 객체 속성의 유지 예제를 확인했다.
원하는 것이 완성 목록인지, 이름별 수정인지 고른다
환경마다 사용할 서버의 전체 집합과 순서를 지정하려면 List를 유지한다. 기본 두 개와 c를 모두 쓰려면 application-dev.yaml의 servers 부분에 세 항목을 전부 적는다. 앞의 by-name 블록은 유지한 채 아래 부분만 바꾼다.
cluster:
servers:
- url: https://a.example.invalid
note: base-a
- url: https://b.example.invalid
note: base-b
- url: https://c.example.invalid
별도 비교에서는 이 완성 목록과 앞의 Map 설정을 application-full.yaml에 넣고 full 하나만 활성화했다. 결과는 list.count=3이었고, 순서대로 a|base-a, b|base-b, c|null에 해당하는 주소가 나왔다. Map은 앞의 dev 결과와 같았다. 이 선택에서는 프로필 파일을 추가분이 아니라 해당 환경의 완성 목록으로 관리한다.
기본 서버를 이름별로 유지하면서 특정 서버를 바꾸거나 추가하려면 Map을 선택한다. 컬렉션 필드 타입과 설정 구조, 소비 코드도 함께 바꿔야 한다. YAML의 목록 기호만 지우면 되는 수정은 아니다. 앞의 출력처럼 기본 note가 남아도 되는지까지 설정 계약에 포함한다.
또한 Map에서 키를 생략하는 것은 삭제 지시가 아니다. dev 파일에 b가 없었지만 결과에는 남았다. 특정 환경에서 기본 서버를 제외해야 하고 전체 목록도 짧다면, List로 완성 목록을 쓰는 쪽이 요구를 직접 표현할 수 있다. Map의 출력 순서는 비교를 위해 정렬한 것이며 접속 우선순위를 검증한 결과는 아니다.
프로필을 켠 것 자체가 List를 지우는지도 따로 확인했다. maponly 프로필에는 by-name.c.url만 넣고 servers는 전혀 적지 않았다. 이때는 기본 List 두 개가 그대로 남고 Map만 세 개가 됐다. 따라서 진단할 때는 활성 프로필뿐 아니라 그 프로필이 같은 List 속성을 다시 정의했는지를 함께 본다.
자기 결과가 다르면 active 행부터 확인한다. dev를 의도했는데 기본값만 나오면 실행 인자와 application-dev.yaml의 실제 위치를 대조한다. active=[dev]인데도 다르다면 다른 설정 출처가 같은 키를 주는지, 진단한 빈이 실제 소비하는 빈과 같은지 확인한다. 개수만 같아도 URL이나 남은 note가 다르면 같은 결과가 아니다.
이 관찰은 Windows, OpenJDK 17.0.2, Spring Boot 3.5.7, Spring Framework 6.2.12, SnakeYAML 2.4에서 얻었다. 온라인 3.5 계열 문서의 표시 버전은 3.5.16이므로 실행 버전의 문서 원본도 별도로 확인했다. 사용자 환경 변수와 기존 앱 설정을 입력에서 제외했으며 HTTP 서버·DB·실제 주소 연결은 사용하지 않았다. 한 번에 활성 프로필 하나만 비교했고, Java 필드에 채운 기본 항목, 생성자 바인딩, 사용자 정의 변환기, 중첩 List를 값으로 가진 Map과 빈 값에 의한 삭제 동작은 실행하지 않았다.