본문 바로가기
빌드노트 검색

빌드노트

ConfigurationProperties Map 키에서 슬래시가 사라지는 이유

ConfigurationProperties로 받은 Map에서 /api가 api로 바뀌면 키 표기를 확인한다. properties 입력 8개를 String·Object 맵에 각각 바인딩해 슬래시 제거와 점의 중첩 해석을 비교하고, 원문 키를 보존할 대괄호 위치를 설명한다.

application.properties에 demo.routes./api=handler를 넣었는데, @ConfigurationProperties로 받은 맵에서 get("/api")가 null이라면 실제 키부터 확인한다. 별도로 실행한 Spring Boot 바인딩 실험에서는 값이 사라진 대신 키가 api로 바뀌었다. 슬래시를 키에 남기려면 demo.routes[/api]=handler처럼 보존할 키 전체를 대괄호로 감싼다.

Spring Boot의 Map 바인딩 문서 (새 탭에서 열림)는 대괄호가 없는 키에서 영숫자·하이픈·점을 제외한 문자를 제거한다고 설명한다. /는 이 제거 대상에 해당한다. 점은 제거되지는 않지만, 맵의 값 타입에 따라 중첩 경로가 될 수 있다.

설정 줄과 실제 맵 키를 맞춰 본다

다음 진단은 demo prefix의 설정 빈에 Map<String,String> routes가 있고, getRoutes()로 읽는 로컬 앱을 전제로 한다. settings는 이미 등록된 그 빈을 주입받은 변수다. 자신의 빈 이름과 getter로 바꿔, 빈 생성 뒤 실행되는 진단 위치나 IDE 디버거에서 키 목록과 원래 키로 조회한 결과를 함께 본다.

System.out.println(settings.getRoutes().keySet());
System.out.println(settings.getRoutes().containsKey("/api"));
System.out.println(settings.getRoutes().get("/api"));

합성 설정 demo.routes./api=handler 한 줄만 넣은 별도 컨텍스트에서 이 진단을 실행한 출력은 다음과 같다. routes라는 Java 필드가 맵이고, /api는 그 안에 넣으려는 키다.

[api]
false
null

이 출력이면 맵 자체가 비어 있는 상황과 구별할 수 있다. 값 handler는 api 아래에 바인딩됐고, /api라는 키가 없어 조회에 실패했다. 이번 실험에서 원본 속성 이름으로 조회한 Environment의 값도 handler였다. 원본 설정을 읽을 수 있다는 것과 바인딩된 맵에 같은 키가 있다는 것은 별개의 확인이다.

슬래시가 업무상 키의 일부라면 로컬 .properties의 기존 줄을 다음 한 줄로 교체한다. 두 줄을 함께 남기지 않고, 비교할 설정 하나씩 적용한다.

demo.routes[/api]=handler

앱을 다시 시작해 같은 진단을 실행한다. 대괄호를 쓴 별도 컨텍스트에서는 아래처럼 바뀌었다. 대괄호는 표기법이고, 실제 키에는 /api만 들어갔다.

[/api]
true
handler

처음부터 키 목록에 /api가 있다면 슬래시 제거로 설명되는 경우가 아니다. 조회하는 문자열과 실제 사용하는 설정 빈을 대조한다. 맵이 비어 있거나 다른 키만 보인다면 적용된 설정 출처·prefix·빈 등록부터 확인한다. 설정 줄 하나만 보고 같은 원인으로 단정할 수는 없다.

점은 맵 값의 타입까지 확인한다

같은 방식으로 점이 든 키를 비교하자 Map<String,String>과 Map<String,Object>의 결과가 갈렸다. 각 입력은 서로 다른 컨텍스트에 한 줄씩 넣었고 값은 모두 문자열 handler였다. 아래 표기는 실제 맵의 출력으로, {a={b=handler}}는 바깥 키 a의 값이 다시 맵이라는 뜻이다.

  • demo.routes.a.b=handler: String 맵은 {a.b=handler}, Object 맵은 {a={b=handler}}였다.
  • demo.routes[a.b]=handler: 두 타입 모두 {a.b=handler}였다.

String 맵의 첫 결과에서는 containsKey("a.b")가 true였고, Object 맵에서는 false였다. Object 맵의 get("a")는 실제로 Map 객체였다. 단순히 출력에서 점 대신 괄호를 보여 준 차이가 아니다.

공식 문서는 String처럼 단일 값으로 바인딩하는 경우 점을 키에 남길 수 있으며, Object는 그 범주에서 제외한다고 설명한다. 이 관찰에 따르면 기존 필드를 Map<String,String>에서 Map<String,Object>로 바꿀 때는 설정을 그대로 두어도 맵의 구조가 달라질 수 있다. 키가 문자 그대로 a.b여야 한다면 [a.b]를 쓴다. a 아래 b라는 중첩 구조를 의도했다면 a.b 경로 표기를 쓴다.

슬래시와 점을 함께 넣은 demo.routes./api.v1=handler도 비교했다. String 맵은 {api.v1=handler}, Object 맵은 {api={v1=handler}}가 됐다. demo.routes[/api.v1]=handler로 바꾸자 둘 다 {/api.v1=handler}였다. 키 전체를 감싸야 슬래시와 점을 함께 원문으로 보존할 수 있었다. 대조군 api-v1은 두 타입 모두 대괄호 유무와 관계없이 그대로 남았다.

이번 실행은 Windows 11, OpenJDK 17.0.2, Spring Boot 3.5.7, Spring Framework 6.2.12에서 했다. 여덟 가지 properties 입력을 두 맵 타입에 바인딩해 총 16개 컨텍스트를 비교했다. OS 환경 변수와 JVM 시스템 속성을 실습용 Spring Environment에서 제외하고, 실제 설정 파일 자동 탐색도 끈 채 합성 properties만 읽었다. 실제 환경 변수나 앱 설정 파일은 변경하지 않았다.

참조한 3.5 웹 문서의 표시 버전은 3.5.16이므로, 실행 버전과 같은 v3.5.7 공식 문서 원본 (새 탭에서 열림)의 Map 바인딩 설명도 대조했다. YAML, 환경 변수 입력, 여러 설정 출처의 우선순위와 다른 Boot 버전은 실행하지 않았다. 이 결과는 properties의 키 표기와 맵 타입을 판단하는 범위로 사용한다.

← 목록으로 돌아가기