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

빌드노트

환경 변수를 넣어도 Spring 설정값이 그대로라면 출처부터 확인한다

Spring Boot 설정이 그대로일 때 JVM에 변수 자체가 없는 경우, 이름이 키에 연결되지 않은 경우, 명령행에 덮인 경우를 구별한다. 다섯 실행의 값·출처와 짧은 한 키 진단 코드로 확인하고, DEMO_ITEM_PRICE도 바인딩된 반례를 함께 설명한다.

Spring Boot의 demo.item-price=10을 환경 변수로 바꿨는데 계속 10이 나온다면, 먼저 실행 중인 JVM이 변수를 받았는지, 그다음 해당 설정의 어느 출처가 선택됐는지를 본다. 문서 규칙으로 만든 이름은 DEMO_ITEMPRICE다. 이름이 맞아도 명령행의 같은 설정이 덮을 수 있으므로 최종 숫자만으로는 원인을 구별하기 어렵다.

이름이 틀린 경우와 값이 덮인 경우는 출력이 달랐다

별도 최소 앱의 application.properties에 demo.item-price=10을 넣고, 이를 @ConfigurationProperties(prefix = "demo") 객체의 int itemPrice로 받았다. 여기서 10은 Java 필드 초기값이 아니라 파일 설정값이다. 매번 새 JVM을 실행해 변수 존재 여부, 객체에 바인딩된 값, 같은 키를 제공하는 출처를 비교했다.

  • 변수 없음: 바인딩값 10, 파일에서 읽음.
  • DEMOITEMPRICE=20: JVM에 변수는 있지만 바인딩값은 10, 파일에서 읽음.
  • DEMO_ITEMPRICE=20: 바인딩값 20, 환경 변수에서 읽음.
  • DEMO_ITEM_PRICE=20: 이 이름도 바인딩값 20, 환경 변수에서 읽음.
  • DEMO_ITEMPRICE=20에 앱 인자 --demo.item-price=30 추가: 바인딩값 30, 명령행에서 읽음.

점까지 없앤 DEMOITEMPRICE는 전달 자체가 실패한 것이 아니다. 변수 존재 여부는 true였지만 demo.item-price의 출처 후보에는 들어오지 않았다. 이름만 DEMO_ITEMPRICE로 바꾸자 환경 변수의 20이 적용됐다. 이 비교에서는 파일값이 남은 원인을 이름 변환으로 좁힐 수 있었다.

공식 환경 변수 바인딩 규칙 (새 탭에서 열림)은 정규 설정 이름의 점을 밑줄로 바꾸고, 하이픈을 제거한 뒤 대문자로 만드는 것이다. demo.item-price의 점 경계는 DEMO_ITEMPRICE에 남아 있어야 한다.

다만 DEMO_ITEM_PRICE도 실패한다고 단정하면 안 된다. 처음에는 이 이름이면 10이 남을 것으로 예상했지만 실제로 20이 바인딩돼 예상 검사가 실패했다. 실행 버전인 Boot 3.5.7의 공식 매퍼 소스 (새 탭에서 열림)는 표준 형식과 레거시 형식을 모두 조회 후보로 만든다. 이 코드가 이번 반례를 설명한다. 새 이름은 문서 규칙으로 만들되, 기존 이름의 성공 여부는 실행 결과로 판단해야 한다.

자기 앱에서는 한 키의 출처만 확인한다

로컬 Boot 앱의 기존 main에서 SpringApplication.run(...) 반환값을 var context에 받고, 그 호출 바로 뒤에 아래 진단을 잠시 넣는다. 두 import는 파일 상단에 둔다. 예제는 합성 정수 키만 출력하며, 자기 설정을 조사할 때는 배열에 실제 입력한 변수 이름을, of(...)에는 점과 하이픈이 있는 원래 설정 키를 적는다.

import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;

// 아래는 main의 SpringApplication.run(...) 호출 뒤에 둔다.
for (var variable : new String[]{"DEMOITEMPRICE", "DEMO_ITEMPRICE", "DEMO_ITEM_PRICE"})
    System.out.println(variable + " present=" + (System.getenv(variable) != null));
var key = ConfigurationPropertyName.of("demo.item-price");
for (var source : ConfigurationPropertySources.get(context.getEnvironment())) {
    var property = source.getConfigurationProperty(key);
    if (property != null)
        System.out.println(property.getValue() + " <- " + property.getOrigin());
}

처음 세 줄은 JVM에 변수가 있는지만 확인한다. 이어지는 출력은 ConfigurationPropertySources.get (새 탭에서 열림)으로 같은 키를 제공하는 출처를 조회한 것이다. 이번 단순 정수 설정에서는 먼저 나온 후보의 값과 실제 객체값이 일치했다. 실제로 주입된 객체값과도 대조해야 하며, 출처 후보만으로 객체의 등록 상태까지 확인할 수는 없다.

환경 변수 20과 명령행 30을 함께 준 실행에서 위의 짧은 진단도 다음과 같이 출력됐다. 파일 위치는 이 실행 JAR 안의 위치다.

DEMOITEMPRICE present=false
DEMO_ITEMPRICE present=true
DEMO_ITEM_PRICE present=false
30 <- "demo.item-price" from property source "commandLineArgs"
20 <- System Environment Property "DEMO_ITEMPRICE"
10 <- class path resource [application.properties] from demo.jar - 1:17

환경 변수의 20은 후보에 남아 있다. 그보다 앞선 명령행의 30이 적용된 것이다. 공식 설정 우선순위 (새 탭에서 열림)도 이 세 종류 사이에서는 명령행 → OS 환경 변수 → 설정 파일 순으로 우선한다고 설명한다. 이 출력이라면 변수 이름을 계속 바꾸기보다 실행 명령이나 IDE의 앱 인자에서 같은 키를 찾는다.

합성 키로 직접 비교한다면 로컬 앱에 앞의 파일 설정을 넣고, 기존 방식으로 demo 설정 객체를 등록한 상태에서 진단을 추가해 다시 패키징한다. JDK 17을 쓸 수 있는 새 PowerShell 세션에서 다음 두 실행을 비교할 수 있다. demo.jar는 자신의 로컬 실행 JAR 이름으로 바꾼다. 각 실행의 출력을 확인하고 앱을 종료한 뒤 다음 줄로 진행한다. 서버가 계속 떠 있으면 Ctrl+C로 종료한다.

$env:DEMO_ITEMPRICE = '20'
java -jar .\demo.jar
java -jar .\demo.jar --demo.item-price=30
Remove-Item Env:\DEMO_ITEMPRICE

별도 최소 앱에서 이 입력도 그대로 실행했다. 첫 실행의 객체값은 20이고 출처 후보는 환경 변수 20·파일 10, 두 번째는 객체값 30과 위의 세 후보가 나왔다. PowerShell의 환경 변수 변경 (새 탭에서 열림)은 현재 세션과 새 자식 프로세스에 적용된다. 이미 실행 중인 JVM의 설정은 셸 변수만 바꿔서는 갱신되지 않는다. IDE로 시작한다면 해당 실행 구성에 변수를 넣고 앱을 재시작해야 한다.

출력이 다를 때는 다음 순서로 범위를 좁힌다.

  1. 실제 입력한 이름의 present=false라면 셸·IDE·컨테이너에서 JVM으로 변수를 전달하는 경로부터 확인한다.
  2. present=true인데 해당 키의 후보에는 환경 변수가 없고 파일값만 남는다면, 점·하이픈을 포함한 원래 키와 변수 이름을 대조한다. 이 예제의 DEMOITEMPRICE가 여기에 해당했다.
  3. 환경 변수 후보 앞에 다른 출처가 있다면 그 출처의 같은 키를 확인한다. 이번에는 commandLineArgs가 원인이었다.

출처는 환경 변수 20인데 실제 객체만 10이라면 이 실험과 다른 상황이다. 객체 등록과 실제로 사용하는 인스턴스를 더 확인해야 한다. 진단 출력이 없으면 수정한 JAR을 실행했는지와 앱 시작 오류부터 본다. getOrigin() (새 탭에서 열림)은 출처를 모르면 null일 수 있으므로 null을 환경 변수 미전달로 해석하지 않는다.

이 관찰은 Windows 11, OpenJDK 17.0.2, Spring Boot 3.5.7, Framework 6.2.12의 단순 정수 바인딩으로 얻었다. 참조 문서의 표시 버전은 3.5.16이며 실행 버전과 구별했다. HTTP 서버·DB·운영 설정은 사용하지 않았다. @Value, 리스트·맵, 프로필 조합, 다른 버전은 실행하지 않았고, setAddCommandLineProperties(false)로 명령행 속성을 끈 앱도 비교하지 않았다. 실제 키를 진단할 때는 민감값을 출력하지 말고 존재 여부와 필요한 출처만 확인한다.

← 전체 글 보기