외부 설정
외부 설정 (Externalized Configuration)
같은 코드로 여러 환경(개발/스테이징/운영)을 돌린다면, 환경마다 달라지는 값은 코드 안이 아니라 밖에서 주입받는 게 훨씬 편리해요. Spring Boot는 이를 위해 외부 설정을 지원하는데, 자바 프로퍼티 파일, YAML, 환경 변수, 커맨드라인 인자 등 다양한 원천을 쓸 수 있어요. 핵심은 "나중에 나오는 설정 원천이 앞선 설정을 덮어쓴다"는 규칙을 이해하는 거예요.
출처: Externalized Configuration (Spring Boot Reference Documentation)
설정 원천의 우선순위
Spring Boot는 PropertySource의 등장 순서를 아주 특정하게 정해서 중복 값이 합리적으로 덮어쓰이게 해요. 뒤에 나오는 원천이 앞에서 정의한 값을 덮어쓸 수 있죠. 전체 순서는 다음과 같아요.
- 기본 프로퍼티 —
SpringApplication.setDefaultProperties(Map)으로 지정 @Configuration클래스의@PropertySource어노테이션 — 이 값은 애플리케이션 컨텍스트가 리프레시될 때까지Environment에 추가되지 않아서,logging.*이나spring.main.*처럼 리프레시 전에 읽히는 프로퍼티를 여기서는 못 건드려요.- 설정 데이터(예:
application.properties파일) random.*프로퍼티만 갖는RandomValuePropertySource- OS 환경 변수
- 자바 시스템 프로퍼티(
System.getProperties()) java:comp/env의 JNDI 속성ServletContextinit 파라미터ServletConfiginit 파라미터SPRING_APPLICATION_JSON의 프로퍼티(환경 변수나 시스템 프로퍼티에 담긴 인라인 JSON)- 커맨드라인 인자
- 테스트의
properties속성(@SpringBootTest와 슬라이스 테스트 어노테이션) - 테스트의
@DynamicPropertySource어노테이션 - 테스트의
@TestPropertySource어노테이션 - devtools가 활성일 때
$HOME/.config/spring-boot디렉터리의 devtools 전역 설정 프로퍼티
설정 데이터 파일도 순서가 있어요. jar 안에 패키징된 application.properties(및 YAML 변형), jar 안의 프로파일 전용 application-{profile}.properties, jar 밖의 application.properties, jar 밖의 프로파일 전용 파일 순으로 뒤에 것이 앞선 것을 덮어요. 같은 위치에 .properties와 YAML 둘 다 있으면 .properties가 우선한다는 점도 기억해 두세요.
환경 변수를 쓸 때 OS가 점(.)을 허용하지 않는 경우가 많아서, spring.config.name 대신 SPRING_CONFIG_NAME처럼 밑줄을 쓰면 돼요.
구체적으로, name 프로퍼티를 쓰는 @Component가 있다고 해볼게요.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class MyBean {
@Value("${name}")
private String name;
// ...
}
클래스패스(예: jar 안)에 기본값을 담은 application.properties를 두고, 새 환경에서는 jar 밖의 application.properties로 name을 덮어쓰면 돼요. 일회성 실험은 java -jar app.jar --name="Spring"처럼 커맨드라인 스위치로도 충분해요. 값이 왜 그렇게 됐는지 궁금하면 액추에이터의 env와 configprops 엔드포인트로 진단할 수 있어요.
JSON 애플리케이션 프로퍼티
환경 변수나 시스템 프로퍼티는 이름 제한이 있어서 못 쓰는 속성 이름이 있을 때, 프로퍼티 블록을 하나의 JSON 구조로 묶을 수 있어요. 시작할 때 spring.application.json 또는 SPRING_APPLICATION_JSON 프로퍼티가 파싱되어 Environment에 들어가요.
$ SPRING_APPLICATION_JSON='{"my":{"name":"test"}}' java -jar myapp.jar
이러면 Environment에 my.name=test가 생겨요. 같은 JSON을 시스템 프로퍼티(-Dspring.application.json=...), 커맨드라인 인자(--spring.application.json=...), 또는 JNDI 변수 java:comp/env/spring.application.json으로 넘길 수도 있어요. JSON의 null 값은 누락 값으로 취급되므로, 낮은 순위의 설정 원천 값을 null로 덮어쓰진 못해요.
외부 애플리케이션 프로퍼티
시작 시 Spring Boot가 application.properties와 application.yaml을 자동으로 찾는 위치는 다음과 같아요.
- 클래스패스 루트, 클래스패스
/config패키지 - 현재 디렉터리, 현재 디렉터리의
config/하위 디렉터리,config/하위 디렉터리의 바로 아래 자식 디렉터리
목록은 우선순위 순이며, 뒤 항목이 앞 항목을 덮어요. 파일 이름을 바꾸고 싶으면 spring.config.name을, 명시적 위치를 쓰고 싶으면 spring.config.location(콤마로 구분한 위치 목록)을 지정하면 돼요.
$ java -jar myproject.jar --spring.config.name=myproject
$ java -jar myproject.jar --spring.config.location=optional:classpath:/default.properties,optional:classpath:/override.properties
spring.config.name, spring.config.location, spring.config.additional-location은 어떤 파일을 로드할지 정하는 데 쓰이므로 환경 프로퍼티(OS 환경 변수·시스템 프로퍼티·커맨드라인 인자)로 정의해야 해요.
YAML 다루기
YAML은 JSON의 상위 집합이라 계층적 설정을 표현하기 편리한데, 클래스패스에 SnakeYAML 라이브러리가 있으면 SpringApplication이 프로퍼티 대신 YAML을 자동 지원해요. 스타터를 쓰면 spring-boot-starter가 SnakeYAML을 함께 제공해요.
YAML 문서는 계층 구조를 flat한 프로퍼티로 변환해야 해요. 리스트도 [index] 역참조를 붙인 프로퍼티 키로 펼쳐져요. 다만 YAML 파일은 @PropertySource나 @TestPropertySource로 로드할 수 없어요. 그런 방식으로 값을 로드해야 한다면 프로퍼티 파일을 써야 해요.
타입 안전 설정 프로퍼티 (@ConfigurationProperties)
프로퍼티 값을 클래스에 바인딩해 타입 안전하게 쓰는 방법을 제공해요. @ConfigurationProperties("my.service")를 붙인 클래스에 설정 값을 매핑하면, 문자열이 아니라 실제 타입(객체·리스트·InetAddress 등)으로 접근할 수 있어요.
JavaBean 방식:
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my.service")
public class MyProperties {
private boolean enabled;
private InetAddress remoteAddress;
private final Security security = new Security();
// getters / setters...
}
public class Security {
private String username;
private String password;
private List<String> roles = new ArrayList<>(Collections.singletonList("USER"));
// getters / setters...
}
생성자 바인딩 방식: 파라미터 생성자가 하나 있으면 생성자 바인딩을 쓰는 것으로 간주해요. @DefaultValue로 누락된 프로퍼티의 기본값을 지정할 수 있고, security 값이 아예 없으면 해당 인스턴스는 null로 바인딩돼요.
import java.net.InetAddress;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
@ConfigurationProperties("my.service")
public class MyProperties {
private final boolean enabled;
private final InetAddress remoteAddress;
private final Security security;
public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) {
this.enabled = enabled;
this.remoteAddress = remoteAddress;
this.security = security;
}
public static class Security {
private final String username;
private final String password;
private final List<String> roles;
public Security(String username, String password, @DefaultValue("USER") List<String> roles) {
this.username = username;
this.password = password;
this.roles = roles;
}
// getters...
}
}
생성자 바인딩 클래스의 중첩 멤버(Security 같은)도 생성자를 통해 바인딩돼요. 생성자 바인딩을 쓰려면 클래스를 @EnableConfigurationProperties 또는 설정 프로퍼티 스캐닝으로 활성화해야 해요. 일반 @Component·@Bean·@Import 빈에는 생성자 바인딩을 쓸 수 없어요. 또 -parameters 플래그로 컴파일해야 하는데, Gradle 플러그인이나 Maven에서 spring-boot-starter-parent를 쓰면 자동으로 설정돼요.
알아두면 좋은 점
- 프로퍼티 이름은 캐노니컬한 형태(소문자 kebab-case)로 플레이스홀더에 쓰는 게 좋아요. 그래야 relaxed binding이
demo.item-price와demo.itemPrice를 모두 잡아주는 같은 로직을 쓸 수 있어요. Optional을@ConfigurationProperties와 함께 쓰는 건 권장하지 않아요. 값이 없으면 빈Optional이 아니라null로 바인딩돼요.- 프로퍼티 이름에
my.service.import처럼 예약 키워드를 쓰려면 필드/생성자 파라미터에@Name어노테이션을 사용하면 돼요.
더 알아보기 (Learn more)
- Spring Boot 프로파일·환경설정 — 환경별로 활성 프로파일을 나누는 방법을 다뤄요.
- Spring Boot 자동 설정 —
@ConfigurationProperties클래스가 어떻게 빈으로 등록되는지 이해하는 데 도움이 돼요. - Spring Boot Actuator 모니터링 —
env·configprops엔드포인트로 실제 바인딩 값을 진단하는 법을 다뤄요.