Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

1. Overview

  • Spring 및 Spring Boot에서 'properties'를 설정하는 법을 배울 것이다.

Property

  • 그런데 잠깐, property가 무엇인가? 직역하면 속성이고 애플리케이션의 행동을 결정하는 여러 요소들을 일컫는 말이다.

  • 이 속성은 크게 external property와 internal property로 나뉘어진다. 전자는 말 그대로 외부(즉, 프로그래머측)에서 해당 애플리케이션을 위해 특정 속성값을 결정한거고, 후자는 스프링 프레임워크 자체에 기본적으로 있는 속성값들을 말한다. external property는 프로그래머가 직접 정의를 한 다음에 그 값을 설정하고, internal property는 우리가 정의하진 않지만, 여러가지 방법으로 안의 값을 바꿔 애플리케이션 동작 방식을 바꾸는 것이 가능하다.

  • 이 때 internal property를 API를 쓰든, config 파일을 쓰든 해서 프로그래머가 값을 바꾸는 것을 internal property configuration을 externalize한다고 표현한다.

  • 요점은 internal이든 external이든 property는 애플리케이션의 행동방식을 결정하는 요소들이며, 이 값들을 바꾸는 방법이 여러가지가 있다는 것이다. 그리고 이 글에서는 property 값들을 설정/바꾸는 법, 그리고 이 property 값들을 Spring Boot에서 어떻게 작동해가지고 애플리케이션의 행동 방식을 다르게 하는지 알아볼 것이다.

2. Register a Properties File via Annotations

  • Spring 3.1부터 @PropertySource라는 annotation이 등장했다.

  • javadoc

  • @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

  • javadoc

  • Environment는 인터페이스의 일종으로, 말 그대로 프로그램의 '환경'을 나타낸다. 이에 해당하는 2가지 핵심 요소가 있는데 바로 profile과 property다.

  • property는 아까 말한 그 property다. 이거랑 관련된 Environment의 역할은 현 애플리케이션 환경 상에 있는 property source들을 다 모으고, 이에 접근하기 용이한 인터페이스를 제공하는 것이다.

  • 다만 이런 인터페이스가 있다는것만 알면 되고 실질적인 상호작용, 특히 애플리케이션 레벨의 bean들이 Environment의 properties를 활용하는 것은 밑에 소개하는 방식들을 통해 접근하는걸 권장한다.

Profile

  • javadoc

  • 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들이 뭔지에 대한 관리를 한다.

2.1 Defining Multiple Property Locations

  • Java 8부터는 @PropertySource를 여러개 사용하는 것이 가능하다. (repeatable annotation 취급) 즉 여러개의 property source를 load하는 것이 가능하다.
@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class PropertiesWithJavaConfig {
    //...
}
  • 위처럼 표기하는게 너무 어지러우면 배열 형식으로 repetable annotation을 집어 넣는 문법을 활용하면 된다.
@PropertySources({
    @PropertySource("classpath:foo.properties"),
    @PropertySource("classpath:bar.properties")
})
public class PropertiesWithJavaConfig {
    //...
}
  • 다만 유의할 부분이 있는데, 서로 다른 properties 파일에서 같은 이름의 property를 정의하는 경우가 있을 수 있다. 이 경우 마지막으로 읽은 property 파일의 값으로 설정이 된다.

3. Using/Injecting Properties

  • 먼저 @Value annotation을 사용해 bean의 특정 field에 property를 주입하는 방법이 있다. @Value annotation은 다음 글에서 자세히 알아볼거고 간략히 언급하자면 Spring bean의 특정 field에 값을 집어넣는 데 사용되는 annotation이다. 밑은 property 이름 중 jdbc.url에 해당하는 녀석에 대응되는 값을 jdbcUrl에 집어넣고 있다. 앞에 소개한 플레이스홀더를 사용 중.
@Value( "${jdbc.url}" )
private String jdbcUrl;
  • 해당 property가 존재하지 않을 때 쓸 기본 값을 설정하는 것도 가능하다.
@Value( "${jdbc.url:aDefaultUrl}" )
private String jdbcUrl;
  • property value 주입이 아닌 추출을 원할 때가 있다. 이 경우 앞에서 소개한 application의 Environment bean을 활용하면 된다.
@Autowired
private Environment env;
...
dataSource.setUrl(env.getProperty("jdbc.url"));

4. Properties With Spring Boot

  • 앞에까지는 Spring에서 제공하는 기능이며, 5번부터 또 Spring에서 추가로 제공하는 기능들에 대해 알아볼거다. 하지만 그 전에 Spring Boot에서 제공하는 property와 관련된 새로운 서비스들이 뭐가 있는지 한번 알아보도록 하자.

  • 뒤에 소개할 내용들의 목표는 Spring Boot의 핵심 목표와 동일한데, property 활용을 위해 기본적으로 설정해야 하는 코드들을 최대한 줄이는 것이다.

4.1 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
  • 이 때 Spring Boot 2.3부터는 와일드카드를 활용하는 것이 가능하다. 여러개의 property source file을 등록하려고 할 때 유용하다. 예를 들어 밑은 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

4.2 Environment-Specific Properties File

  • 앞이랑 비슷한 용도의 기능이다. 우리가 각 profile별로 사용할 .properites를 달리하고 싶다고 해보자. 이를 Spring Boot에서는 간단하게 해결해준다.
    • 먼저 특정 profile에 대응되는 .propertiessrc/main/resources에 저장한다. 예를들어 profile 이름이 dev이면 application-dev.properties로 등록해야 한다.
    • 그 다음에 default properties 파일인 application.properties에서 spring.profile.active 환경변수를 원하는 profile로 설정한다. 예를 들어 dev profile을 사용하고 싶으면 spring.profile.active=dev로 해야 한다. (여러개도 가능함)
    • 이후 실행을 하면 Spring Boot에서는 자동으로 기본 properties 파일과 각 profile의 properties파일을 load한다. 참고로 각 profile에서의 property 값이 default profile에서 설정된 값을 오버라이딩한다는 점 유의.

4.3 Test-Specific Properties File

  • 테스트 때 사용하고 싶은 property가 다를 수도 있다.

  • Spring Boot의 경우 테스트 실행 시 src/test/resources에 있는 property 파일을 참고해서 테스트 때만 사용할 property들을 파악한다. 이 때 기본 property도 같이 load되지만 test용에서 따로 정의한게 있으면 그 값으로 오버라이딩 된다.

4.4 The @TestPropertySource Annotation

  • test때 사용할 `.properties가 뭔지를 구체적으로 정하는 것도 가능하다. @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");
    }
}
@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");
    }
}

4.5 Hierarchical Properties

  • 앞에 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
}

4.6 Alternative : YAML Files

  • 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 지원을 하지 않는다.
    • 얘가 앞의 multi-document property file의 근원이고 이를 기본으로 지원하는데, Spring Boot 2.4.0 이전의 경우 오버라이딩 기준이 profile 활성화 순서였다. 하지만 Spring Boot 2.4.0부터 앞의 multi-document .properties property file처럼 활성화된 애들 중 가장 밑에 있는 녀석의 값으로 오버라이드 되는 것으로 정책이 바꿔졌다.
    • 또 2.4.0부터 특정 profile에 대한 property 설정을 하는 영역에서 profile을 활성화하는 것도 불가능해짐. 즉 profile이 이거 activate 되고 그러면 저게 activate되고 하는 난잡함이 좀 더 줄어들었다.

4.7 Importing Additional configuration Files

  • 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를 활용해야 한다. 여기서 몇가지 추가 기능들도 제공하는데

    • 한번에 여러개의 파일/디렉토리 등록 가능
    • classpath나 외부 경로(파일 경로) 활용 가능.
    • 가동에 '꼭' 필요한 property 파일이라 없으면 오류를 내야 하는지, 아니면 있으면 좋고 없으면 말고 형식의 선택적인 파일인지 명시 가능
    • 확장자 없는 파일 파일 등록 가능. 이 때 확장자를 뭘로 할지 설정도 가능.
  • 예를 들어 밑의 경우를 보도록 하자.

spring.config.import=classpath:additional-application.properties,
  classpath:additional-application[.yml],
  optional:file:./external.properties,
  classpath:additional-application-properties/
  • 이 파일이 명시하는 것은
    * classpath에 있는 additional-application.properties 추가. 없으면 오류.
    • classpath의 additional-application이라는 확장자가 없는 파일을 등록. 이때 해당 파일의 확장자는 .yml이라고 생각하고 등록하라고 명시 중이다.
    • 외부, 즉 파일 경로를 통해 파일 추가. 현재 working directory의 external.properties를 추가하라고 하고 있다. 없어도 오류를 내지 않는다.
    • classpath의 additional-application-properties라는 directory 하위의 모든 property file을 추가.
  • 이 documentation도 도움이 많이 된다.

4.8 Properties From Command Line Arguments

  • command line에 직접 property 값을 전달하는 것도 가능하다. 밑은 property라는 이름을 가지는 property가 value라는 값을 가지는 것을 타나낸다. 헷갈리지 말아야 하는 것이 -- 뒤의 property가 해당 property의 이름이다.
java -jar app.jar --property="value"
  • 아니면 JVM option을 활용해가지고 property로 등록하는 것도 가능하다. 자세한 문법은 이 글 참고.
java -Dproperty.name="value" -jar app.jar

4.9 Properties from Environment Variables

  • Spring Boot의 경우 환경 변수에 있는 값도 property로 취급을 한다. 그래서 환경변수를 미리 설정하고 애플리케이션을 실행하면 그 값을 property 형태로 사용이 가능하다.
export name=value
java -jar app.jar

4.10 Randomization of Property Values

  • property 값을 랜덤으로 설정하는 것도 가능하다.
random.number=${random.int}
random.long=${random.long}
random.uuid=${random.uuid}
  • 여기서 number, long, uuid property를 설정하는데 RandomValuePropertySource를 활용하고 있다. (관련 javadoc)

  • 이 중 random.int라는 문법과 random.long 문법은 추가 제약사항을 넣는 것이 가능하다. 숫자를 랜덤으로 생성하는건... 뭐 직관적으로 나오고. 나머지는 문자열을 랜덤으로 만든다. 더 자세한 제약사항 추가 방법은 앞의 javadoc이나 이 글 참고

4.11 Additional Types of Property Sources

  • 사실 이 외에도 외부에서 property를 설정하는 방법이 매우 많다. 공식 문서에 따르면 가능한 방법은 크게 총 15가지다.

  • 이걸 다 외우...면 좋겠지만 머리가 아프니 그냥 이게 있다는걸 인지하고 필요할 때 찾는 것이 더 좋다고 생각한다. 이 15가지의 property 설정 방식은 우선순위가 존재하기 때문에 최종적으로 override되는 값이 뭐가 되는지 역추적 하는데도 도움이 된다는 점 유의

5. Configuration Using Raw Beans - the PropertySourcesPlaceholderConfigurer

  • 앞의 경우랑 많이 이질적인 property 설정 방식으로 property를 구성하는 bean을 그냥 직접 만드는 것도 가능하다.
@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를 어떻게 로드할지 프로그래밍이 가능하지만 꽤 귀찮고 보기가 어렵다는 단점이 있다.

6. Properties in Parent-Child Contexts

  • 웹 애플리케이션에 context가 여러개가 있는 경우, 그러니까 부모랑 자식 context가 존재하는 경우 property file을 어떻게 작성해야 하는가에 대한 애로사항이 자주 생기는 편이다. parent의 경우에는 핵심 기능 및 핵심 bean들을 보통 보유할거고 여러개의 child context를 하위로 둘 것이며, 보통 이 하위 context들은 servlet에 해당하는 bean일 확률이 높음.

  • 이 때 이 포함관계를 고려해가지고 property file들을 정의하는 것이 좋다.
    * Parent context에서 참고하는 property file에 속한 property는 parent랑 child가 @Value로 주입받거나 Environment.getProperty로 취득하는 것이 가능

    • Child context에서만 참고하는 property file에 속한 property는 child만 @Value로 주입받거나 Environment.getProperty로 취득하는 것이 가능하며, parent는 이 둘이 다 불가능하다.
profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글