그런데 잠깐, property가 무엇인가? 직역하면 속성이고 애플리케이션의 행동을 결정하는 여러 요소들을 일컫는 말이다.
이 속성은 크게 external property와 internal property로 나뉘어진다. 전자는 말 그대로 외부(즉, 프로그래머측)에서 해당 애플리케이션을 위해 특정 속성값을 결정한거고, 후자는 스프링 프레임워크 자체에 기본적으로 있는 속성값들을 말한다. external property는 프로그래머가 직접 정의를 한 다음에 그 값을 설정하고, internal property는 우리가 정의하진 않지만, 여러가지 방법으로 안의 값을 바꿔 애플리케이션 동작 방식을 바꾸는 것이 가능하다.
이 때 internal property를 API를 쓰든, config 파일을 쓰든 해서 프로그래머가 값을 바꾸는 것을 internal property configuration을 externalize한다고 표현한다.
요점은 internal이든 external이든 property는 애플리케이션의 행동방식을 결정하는 요소들이며, 이 값들을 바꾸는 방법이 여러가지가 있다는 것이다. 그리고 이 글에서는 property 값들을 설정/바꾸는 법, 그리고 이 property 값들을 Spring Boot에서 어떻게 작동해가지고 애플리케이션의 행동 방식을 다르게 하는지 알아볼 것이다.
Spring 3.1부터 @PropertySource
라는 annotation이 등장했다.
@Configuration
annotation이랑 같이 쓰인다. Spring의 Environment
에다가 PropertySource
를 추가하는데 사용이 된다. 이 때 위치 지정은 classpath로 해도 되고 그냥 일반 파일 형식으로 하는 것도 가능하다. 밑의 예제는 /com/myco/
라는 package에 있는 app.properties
라는 파일을 Environment
의 property source로 새로 등록하는 것이다. 앞의 classpath:
는 그 다음 경로가 classpath라는 것을 표기하는데 사용된다.
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class PropertiesWithJavaConfig {
//...
}
${...}
을 사용해 동적으로 property source로 등록할 파일 경로를 결정하는 것도 가능하다. ${...}
라는 플레이스홀더를 활용해 Environment
에 이미 등록되어 있는 property를 여기에 집어넣으라는 형식으로 말이다. 2번째 예제의 경우 pom.xml
이나 build.gradle
의 dependency도 classpath에 포함된다는 점도 활용했다.@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
...
}
@PropertySource({
"classpath:persistence-${envTarget:mysql}.properties"
})
${...}
placeholder문법의 일종인데 저 중괄호 안의 property에 해당하는 값을 집어넣는 것을 의미한다. :
이랑 그 옆에 특정 값이 있는 경우도 있는데 해당 property 값이 존재 안하면 그 값을 기본값으로 설정하겠다는 것을 의미.
Environment
Environment
는 인터페이스의 일종으로, 말 그대로 프로그램의 '환경'을 나타낸다. 이에 해당하는 2가지 핵심 요소가 있는데 바로 profile과 property다.
property는 아까 말한 그 property다. 이거랑 관련된 Environment
의 역할은 현 애플리케이션 환경 상에 있는 property source들을 다 모으고, 이에 접근하기 용이한 인터페이스를 제공하는 것이다.
다만 이런 인터페이스가 있다는것만 알면 되고 실질적인 상호작용, 특히 애플리케이션 레벨의 bean들이 Environment
의 properties를 활용하는 것은 밑에 소개하는 방식들을 통해 접근하는걸 권장한다.
Profile
profile은 bean들의 정의를 특정 그룹으로 분류해서 모아놓은 것인데, 이들 중 active한 profile의 bean들만이 만들어져서 container에 등록된다. 즉 개발/테스트/디버깅 등의 상황에 따라 만들어야 할 bean들이 무엇인지를 모아놓은 다음에 각 상황에 따라 그 그룹에 해당하는 bean들을 container에 등록한다는 것이다.
이 때 뭐가 active인지 결정하는 방법은 ConfigurableEnvironment.setActiveProfiles(java.lang.String)
로 설정하거나, spring.profiles.active
property를 통해 설정하는 방법이 있다. 아니면 환경변수 수정 / 서블릿의 web.xml
수정을 통해 설정도 가능.
참고로 값이 설정 안될 수도 있는데, 이 경우 'default'라는 이름의 프로파일에 등록된 bean들이 container에 들어간다. 이것의 설정은 ConfigurableEnvironment.setDefaultProfiles(java.lang.String)
로 설정하거나 spring.profiles.default
property를 통해 설정하는 방법이 있다. 또는 환경변수 수정 / 서블릿의 web.xml
수정을 통해 설정도 가능.
앞의 Environment
은 active profile, 그리고 아무것도 명시되지 않았을 때 실행해야 할 default profile들이 뭔지에 대한 관리를 한다.
@PropertySource
를 여러개 사용하는 것이 가능하다. (repeatable annotation 취급) 즉 여러개의 property source를 load하는 것이 가능하다.@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class PropertiesWithJavaConfig {
//...
}
@PropertySources({
@PropertySource("classpath:foo.properties"),
@PropertySource("classpath:bar.properties")
})
public class PropertiesWithJavaConfig {
//...
}
@Value
annotation을 사용해 bean의 특정 field에 property를 주입하는 방법이 있다. @Value
annotation은 다음 글에서 자세히 알아볼거고 간략히 언급하자면 Spring bean의 특정 field에 값을 집어넣는 데 사용되는 annotation이다. 밑은 property 이름 중 jdbc.url
에 해당하는 녀석에 대응되는 값을 jdbcUrl
에 집어넣고 있다. 앞에 소개한 플레이스홀더를 사용 중.@Value( "${jdbc.url}" )
private String jdbcUrl;
@Value( "${jdbc.url:aDefaultUrl}" )
private String jdbcUrl;
Environment
bean을 활용하면 된다.@Autowired
private Environment env;
...
dataSource.setUrl(env.getProperty("jdbc.url"));
앞에까지는 Spring에서 제공하는 기능이며, 5번부터 또 Spring에서 추가로 제공하는 기능들에 대해 알아볼거다. 하지만 그 전에 Spring Boot에서 제공하는 property와 관련된 새로운 서비스들이 뭐가 있는지 한번 알아보도록 하자.
뒤에 소개할 내용들의 목표는 Spring Boot의 핵심 목표와 동일한데, property 활용을 위해 기본적으로 설정해야 하는 코드들을 최대한 줄이는 것이다.
application.properties
: the Default Property File먼저 Spring Boot project는 따로 property source를 제공할 파일을 @PropertySource
를 통해 explicit하게 등록할 필요가 없다. 왜냐하면 기본적으로 src/main/resources
에 있는 application.properties
를 property source로 등록하고 있기 때문이다.
또 command line argument를 통해 spring.config.locations
이라는 환경 변수를 바궈가지고 다른 property source file을 기본 property source로 제공하는 것도 가능하다. 밑은 another-location.properties
를 기본 property source로 사용하고 실행하라는 것을 의미.
java -jar app.jar --spring.config.location=classpath:/another-location.properties
config/
하위에 있는 모든 configuration 파일을 property source file로 등록한다.java -jar app.jar --spring.config.location=config/*/
spring Boot 2.4.0부터는 또 새로운 기능을 제공하는데, multi-document properties file이다. 하나의 property source 파일 내에 각기 다른 profile에 대해 property를 어떻게 설정할지를 알려주는 파일이다.*
이 파일의 맨 위는 default profile, 그러니까 기본적으로 설정할 property들을 먼저 적는다. 그 다음에 #---
로 구별을 해서 밑에다가 각 profile에 대해 각 property를 어떻게 설정해야 하는지를 다 명시한다. 이 때 해당 property가 위에서 이미 정의한 property인 경우 오버라이드 한다. 이러면 좋은 점이, 상황에 따라 load할 파일 명시를 바꾸고, 또 여러 파일을 관리해야 하는 귀찮음을 많이 줄여준다. 이는 사실 YAML 형식의 properties 문법을 차용한거라고...
밑은 기본 설정, dev
profile 활성화시 추가 설정, prod
profile 활성화시 추가 설정을 명시한 property source 파일이다.
logging.file.name=myapplication.log
bael.property=defaultValue
#---
spring.config.activate.on-profile=dev
spring.datasource.password=password
spring.datasource.url=jdbc:h2:dev
spring.datasource.username=SA
bael.property=devValue
#---
spring.config.activate.on-profile=prod
spring.datasource.password=password
spring.datasource.url=jdbc:h2:prod
spring.datasource.username=prodUser
bael.property=prodValue
.properites
를 달리하고 싶다고 해보자. 이를 Spring Boot에서는 간단하게 해결해준다..properties
를 src/main/resources
에 저장한다. 예를들어 profile 이름이 dev이면 application-dev.properties
로 등록해야 한다.application.properties
에서 spring.profile.active
환경변수를 원하는 profile로 설정한다. 예를 들어 dev profile을 사용하고 싶으면 spring.profile.active=dev
로 해야 한다. (여러개도 가능함)테스트 때 사용하고 싶은 property가 다를 수도 있다.
Spring Boot의 경우 테스트 실행 시 src/test/resources
에 있는 property 파일을 참고해서 테스트 때만 사용할 property들을 파악한다. 이 때 기본 property도 같이 load되지만 test용에서 따로 정의한게 있으면 그 값으로 오버라이딩 된다.
@TestPropertySource
Annotation@TestPropertySource
를 활용한다. 밑은 src/test/resources/foo.properties
를 해당 테스트에서 사용하겠다는 것을 의미.@RunWith(SpringRunner.class)
@TestPropertySource("/foo.properties")
public class FilePropertyInjectionUnitTest {
@Value("${foo}")
private String foo;
@Test
public void whenFilePropertyProvided_thenProperlyInjected() {
assertThat(foo).isEqualTo("bar");
}
}
파일을 제공하지 않고 그냥 사용할 property 값들을 일일이 지정하는 것도 가능하다.
@RunWith(SpringRunner.class)
@TestPropertySource(properties = {"foo=bar"})
public class PropertyInjectionUnitTest {
@Value("${foo}")
private String foo;
@Test
public void whenPropertyProvided_thenProperlyInjected() {
assertThat(foo).isEqualTo("bar");
}
}
@SpringBootTest
라는 annotation의 properties
를 활용하는 것도 가능하다. (@SpringBootTest
javadoc의 properties
부분 참고. 이전에 몇번 나온 annotation인데, 이 글 핵심 내용은 아니어서 자세히 다루지는 않겠다.@RunWith(SpringRunner.class)
@SpringBootTest(
properties = {"foo=bar"}, classes = SpringBootPropertiesTestApplication.class)
public class SpringBootPropertyInjectionIntegrationTest {
@Value("${foo}")
private String foo;
@Test
public void whenSpringBootPropertyProvided_thenProperlyInjected() {
assertThat(foo).isEqualTo("bar");
}
}
앞에 property 관련 내용물을 보면 몇몇개는 .
으로 끝나는 무슨 공통된 단어들이 앞에 붙어 있고 몇몇개는 그런게 아예 없는 것을 볼 수가 있다. 이 .
으로 끝나는 단어들은 property를 특정 그룹으로 묶는 효과를 가지고 있으며 이거랑 @ConfigurationProperties
라는 annotation을 활용해 property들을 계층 구조로 관리 및 활용하는 것이 가능하다.
밑의 property source 파일을 예시로 들어보자. 전부 database.
그룹에 속해 있다고 볼 수 있다.
database.url=jdbc:postgresql:/localhost:5432/instance
database.username=foo
database.password=bar
@ConfigurationProperties
javadoc을 보면 @Configuration
안의 @Bean
method나 그냥 class 정의에다가 이 annotation을 달면 external property 중 주입에 유효한 property들이 뭔지를 prefix를 통해 설정하는 것이 가능하다고 나와 있다. 여기서 prefix는 위의 .
으로 끝나는 단어들을 일컫는 말이다.
그래서 밑과 같이 하면 자동으로 각 field에 jdbc:postgresql:/localhost:5432/instance
, foo
, bar
이 주입된다.
@ConfigurationProperties(prefix = "database")
public class Database {
String url;
String username;
String password;
// standard getters and setters
}
YAML로 property source 파일을 작성하는 것도 가능하다. 이건 일반 Spring도 지원한다. 유의점은 SnakeYAML이라는 dependency를 추가해야 한다 정도...?
계층관계 형태의 property 보관에 큰 강점을 보인다. 예를 들어 밑 2개는 .properties
랑 .yml
을 사용해서 같은 property source 파일을 나타낸 것이다.
database.url=jdbc:postgresql:/localhost:5432/instance
database.username=foo
database.password=bar
secret: foo
database:
url: jdbc:postgresql:/localhost:5432/instance
username: foo
password: bar
secret: foo
@PropertySource
지원을 하지 않는다..properties
property file처럼 활성화된 애들 중 가장 밑에 있는 녀석의 값으로 오버라이드 되는 것으로 정책이 바꿔졌다.2.4.0 이전의 Spring Boot를 보면 앞의 4.1에서 언급했듯이 spring.config.location
을 활용하거나 spring.config.additional-location
을 활용해서 추가 configuration 파일들을 넣는 것이 가능했다.
그런데 앞에서 그 값을 command line argument 형태로 전달하는 것을 봤다. 왜 그러냐면, 이거 .properties
파일에 설정하는 것이 불가능하다. 정확히는 애플리케이션 아예 시작 전에 무조건 어떻게든 전달되어야 한다. 그래서 앞에처럼 command line argument로 전달하거나 시스템 환경 변수를 미리 설정하는것 말고는 답이 없었다.
그러나 2.4.0부터는 .properties
나 .yml
에다가 property source 파일 추가가 가능하다. 이 경우 spring.config.import
를 활용해야 한다. 여기서 몇가지 추가 기능들도 제공하는데
예를 들어 밑의 경우를 보도록 하자.
spring.config.import=classpath:additional-application.properties,
classpath:additional-application[.yml],
optional:file:./external.properties,
classpath:additional-application-properties/
additional-application.properties
추가. 없으면 오류.additional-application
이라는 확장자가 없는 파일을 등록. 이때 해당 파일의 확장자는 .yml
이라고 생각하고 등록하라고 명시 중이다.external.properties
를 추가하라고 하고 있다. 없어도 오류를 내지 않는다.additional-application-properties
라는 directory 하위의 모든 property file을 추가.property
라는 이름을 가지는 property가 value
라는 값을 가지는 것을 타나낸다. 헷갈리지 말아야 하는 것이 --
뒤의 property
가 해당 property의 이름이다.java -jar app.jar --property="value"
java -Dproperty.name="value" -jar app.jar
export name=value
java -jar app.jar
random.number=${random.int}
random.long=${random.long}
random.uuid=${random.uuid}
여기서 number
, long
, uuid
property를 설정하는데 RandomValuePropertySource
를 활용하고 있다. (관련 javadoc)
이 중 random.int
라는 문법과 random.long
문법은 추가 제약사항을 넣는 것이 가능하다. 숫자를 랜덤으로 생성하는건... 뭐 직관적으로 나오고. 나머지는 문자열을 랜덤으로 만든다. 더 자세한 제약사항 추가 방법은 앞의 javadoc이나 이 글 참고
사실 이 외에도 외부에서 property를 설정하는 방법이 매우 많다. 공식 문서에 따르면 가능한 방법은 크게 총 15가지다.
이걸 다 외우...면 좋겠지만 머리가 아프니 그냥 이게 있다는걸 인지하고 필요할 때 찾는 것이 더 좋다고 생각한다. 이 15가지의 property 설정 방식은 우선순위가 존재하기 때문에 최종적으로 override되는 값이 뭐가 되는지 역추적 하는데도 도움이 된다는 점 유의
PropertySourcesPlaceholderConfigurer
@Bean
public static PropertySourcesPlaceholderConfigurer properties(){
PropertySourcesPlaceholderConfigurer pspc
= new PropertySourcesPlaceholderConfigurer();
Resource[] resources = new ClassPathResource[ ]
{ new ClassPathResource( "foo.properties" ) };
pspc.setLocations( resources );
pspc.setIgnoreUnresolvablePlaceholders( true );
return pspc;
}
이 때 사용하는 class는 PropertySourcesPlaceholderConfigurer
이라는 class다. (javadoc)
위 예시는 먼저 ClassPathResource
배열을 만들고 있는데 property source를 load할 classpath 기반 source들에 대한 배열이다. (javadoc)
그다음에 해당 source들에서 property를 load할것이라고 명시하고 (setLocations
) 해결 불가능한 placeholder들의 경우에는 일단 무시하도록 설정까지 한다음 (setIgnoreUnresolvablePlaceholders
) 이 bean을 그대로 반환.
이것은 앞에 소개한 '외부' property 설정이랑 완전 별개다. 프로그램 내부에서 설정 bean을 만든다음에 이를 반환한 것이기 때문. 매우 구체적으로 property를 어떻게 로드할지 프로그래밍이 가능하지만 꽤 귀찮고 보기가 어렵다는 단점이 있다.
웹 애플리케이션에 context가 여러개가 있는 경우, 그러니까 부모랑 자식 context가 존재하는 경우 property file을 어떻게 작성해야 하는가에 대한 애로사항이 자주 생기는 편이다. parent의 경우에는 핵심 기능 및 핵심 bean들을 보통 보유할거고 여러개의 child context를 하위로 둘 것이며, 보통 이 하위 context들은 servlet에 해당하는 bean일 확률이 높음.
이 때 이 포함관계를 고려해가지고 property file들을 정의하는 것이 좋다.
* Parent context에서 참고하는 property file에 속한 property는 parent랑 child가 @Value
로 주입받거나 Environment.getProperty
로 취득하는 것이 가능
@Value
로 주입받거나 Environment.getProperty
로 취득하는 것이 가능하며, parent는 이 둘이 다 불가능하다.