[Spring Boot #9] 스프링 부트 외부 설정, 설정값 검증

| 스프링 부트 외부 설정


스프링 부트는 외부 설정을 통해 스프링 부트 어플리케이션의 환경설정 혹은 설정값을 정할 수 있습니다. 스프링 부트에서 사용할 수 있는 외부 설정은 크게 properties, YAML, 환경변수, 커맨드 라인 인수 등이 있습니다.


| properties 파일을 통한 설정


properties 파일을 통해서 다음과 같이 스프링 부트 어플리케이션의 외부 설정을 할 수 있습니다. properties의 값은 @Value 어노테이션을 통해 읽어올 수 있습니다.

# application.properties
# 스프링부트가 구동될 때 자동적으로 로딩하는 프로퍼티 파일
# 스프링부트의 규약이라고 볼 수 있음
saelobi.name=KBS
saelobi.age=${random.int}
# 플레이스 홀더
saelobi.fullName=${saelobi.name} Kim
@Component
public class AppRunner implements ApplicationRunner {

@Value("${saelobi.name}")
private String name;

@Value("${saelobi.age}")
private int age;

@Value("${saelobi.fullName}")
private String fullName;

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("=============");
System.out.println(name);
System.out.println(age);
System.out.println(fullName);
System.out.println("=============");
}
}

=============
KBS
-1513209138
KBS Kim
=============


| 프로퍼티 우선 순위


스프링 부트에서는 프로퍼티값에 대해 우선 순위를 두고 있으며 우선 순위는 다음과 같습니다.


1. 유저 홈 디렉토리에 있는 spring-boot-dev-tools.properties

2. 테스트에 있는 @TestPropertySource

3. @SpringBootTest 애노테이션의 properties 애트리뷰트

4. 커맨드 라인 아규먼트

5. SPRING_APPLICATION_JSON (환경 변수 또는 시스템 프로티) 에 들어있는

프로퍼티

6. ServletConfig 파라미터

7. ServletContext 파라미터

8. java:comp/env JNDI 애트리뷰트

9. System.getProperties() 자바 시스템 프로퍼티

10. OS 환경 변수

11. RandomValuePropertySource

12. JAR 밖에 있는 특정 프로파일용 application properties

13. JAR 안에 있는 특정 프로파일용 application properties

14. JAR 밖에 있는 application properties

15. JAR 안에 있는 application properties

16. @PropertySource

17. 기본 프로퍼티 (SpringApplication.setDefaultProperties)


| 프로퍼티 우선 순위 확인해보기


커맨드 라인 아규먼트는 프로퍼티 우선 순위에서 4순위로 JAR 안에 있는 특정 프로파일용 application.properties 파일(13순위)보다 우선순위가 높습니다.


따라서 스프링 부트 어플리케이션을 mvn package를 통하여 패키지한 다음 커맨드 라인 아규먼트를 줘서 실행하면 커맨드 라인 아규먼트의 우선순위가 높으므로 인수값이 오버라이딩 되어 출력됩니다. (원래 application.properties saelobi.name 값은 KBS였지만 SuperMan으로 바뀜)

jara -jar target\springboot-tutorial-1.0-SNAPSHOT.jar --saelobi.name=SuperMan
=============
SuperMan
135839229
SuperMan Kim
=============


스프링 부트 어플리케이션의 테스트 코드를 작성하여 확인할 때 application.properties 파일을 따로 만들어 테스트 용도로 설정해서 쓸 수 있습니다. 이때, 테스트에 있는 application.properties 파일은 본래 src에서 쓰이는 application.properties 파일을 오버라이딩합니다.

# application.properties (test)
saelobi.name=KIMBSYU
saelobi.age=${random.int}
# 플레이스 홀더
saelobi.fullName=${saelobi.name} Kim

// Test code를 실행할 때 src에 있는 클래스파일과 리소스파일이 classpath안으로 들어간다.
// 그 다음 테스트 코드를 컴파일하며 test 밑의 모든 파일들이 classpath안으로 들어간다.
// 이때 test안의 application.properties가 테스트용도로 바뀐다
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringApplicationTests {

@Autowired
Environment environment;

@Test
public void contextLoads(){
assertThat(environment.getProperty("saelobi.name"))
.isEqualTo("KIMBSYU");
}
}


여기서 주의해야 할 것은 만일 src에 있는 application.properties파일에 새로운 설정값을 추가할 경우 test에 있는 application.properties 파일은 이 값을 오버라이딩하게 되어 새 설정값을 반영하지 못하는 문제가 발생합니다. 따라서 application.properties의 새로운 값을 test 소스에 있는 application.properties 파일에 반영하지 않았을 경우에 자바 소스에서 해당 설정값을 쓰게 되면 에러를 내게 됩니다.

@Component
public class AppRunner implements ApplicationRunner {

@Value("${saelobi.name}")
private String name;

@Value("${saelobi.age}")
private int age;

@Value("${saelobi.fullName}")
private String fullName;

@Value("${salobi.job}")
private String job;

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("=============");
System.out.println(name);
System.out.println(age);
System.out.println(fullName);
System.out.println(job);
System.out.println("=============");
}
}
saelobi.name=KBS
saelobi.age=${random.int}
saelobi.job=Engineer
# 플레이스 홀더
saelobi.fullName=${saelobi.name} Kim
# application.properties (test)
saelobi.name=KIMBSYU
saelobi.age=${random.int}
# 플레이스 홀더
saelobi.fullName=${saelobi.name} Kim
Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'salobi.job' in value "${salobi.job}"


위 문제를 해결하기 위해서는 쉽게 testapplication.properties 파일에 saelobi.job 설정값을 추가하면 됩니다. 하지만 이 방법은 테스트 용도 목적으로 설정 파일을 계속해서 수정해야하기 때문에 번거롭습니다.


따라서 testapplication.properties를 지운 후, @TestPropertySource 어노테이션을 사용하여 해당 파일의 값을 선언한 뒤 사용하거나 따로 properties파일을 만든 뒤 설정값을 입력하고 로딩하여 사용하는 방법이 주로 이용됩니다. (2순위로 우선 순위가 높음)

혹은 @SpringBootTest 어노테이션에 설정값을 지정하여 사용할 수 있습니다. (3순위)

// Test code를 실행할 때 src에 있는 클래스파일과 리소스파일이 classpath안으로 들어간다.
// 그 다음 테스트 코드를 컴파일하며 test 밑의 모든 파일들이 classpath안으로 들어간다.
// 이때 test안의 application.properties가 테스트용도로 바뀐다
@RunWith(SpringRunner.class)
//@TestPropertySource(locations="classpath:/test.properties")
@TestPropertySource(properties = "saelobi.job=SuperMan")
@SpringBootTest
//@SpringBootTest(properties = "saelobi.job=BatMan") // 3순위
public class SpringApplicationTests {

@Autowired
Environment environment;

@Test
public void contextLoads(){
assertThat(environment.getProperty("saelobi.job"))
.isEqualTo("SuperMan");
}
}


| application.properties 파일 위치에 따른 우선 순위


스프링 부트에서는 application.properties 의 위치에 따라서 설정값의 우선 순위가 결정됩니다. 그 우선 순위는 다음과 같습니다.


1. file:./config/ (프로젝트 root 디렉터리의 /config 디렉터리)

2. file:./ (프로젝트 root 디렉터리)

3. classpath:/config/ (클래스패스(java jar 실행 위치)의 config 디렉터리)

4. classpath:/ (클래스패스 디렉터리)


# file:../ (프로젝트 root 디렉터리) application.properties
saelobi.name=strongman
=============
strongman
1415720284
strongman Kim
Engineer
=============


| @ConfigurationProperties 어노테이션을 통한 외부 설정값 주입


위에서 외부 설정 파일을 통해 설정값을 입력한 후 @Value 어노테이션을 통해 설정값을 주입하는 방법을 알아보았습니다. 하지만 이 방법보다 @ConfigurationProperties 프로퍼티 파일의 값을 받은 클래스를 하나 생성하여 그 클래스를 @Autowired 같은 어노테이션을 통해 자동 주입하는 방법이 type-safe, 유지보수 측면에서 더 좋은 장점을 가집니다.

# application.properties
# 스프링부트가 구동될 때 자동적으로 로딩하는 프로퍼티 파일
# 스프링부트의 규약이라고 볼 수 있음
saelobi.name=KBS
saelobi.age=${random.int(10,100)}
# 플레이스 홀더
saelobi.fullName=${saelobi.name} Kim

@Component
@ConfigurationProperties("saelobi") // 인수로는 prefix 지정자를 받음
public class SaelobiProperties {

private String name; // prefix 지정자 때문에 saelobi.name 을 프로퍼티 파일에서 읽어옴

private int age; // saelobi.age

private String fullName; // saelobi.fullName

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}
}
@Component
public class AppRunner implements ApplicationRunner {

@Autowired
SaelobiProperties saelobiProperties;

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("=============");
System.out.println(saelobiProperties.getName());
System.out.println(saelobiProperties.getAge());
System.out.println(saelobiProperties.getFullName());
System.out.println("=============");
}
}
=============
KBS
54
KBS Kim
=============


| 융통성 있는 바인딩, 타입 컨버전(Relaxed Binding, Type Conversion)


프로퍼티 파일에 키값을 다음과 같이 키값으로 설정해도 스프링 부트에서 자동적으로 fullName 변수에 바인딩 해주는 기능입니다. 


  • context-path(케밥)
  • context_path(언더스코어)
  • contextPath(캐멀)
  • CONTEXTPATH

saelobi.name=KBS
saelobi.age=${random.int(10,100)}
saelobi.full_name=${saelobi.name} Kim
@Component
@ConfigurationProperties("saelobi") // 인수로는 prefix 지정자를 받음
public class SaelobiProperties {

private String name; // prefix 지정자 때문에 saelobi.name 을 프로퍼티 파일에서 읽어옴

private int age; // saelobi.age

private String fullName; // saelobi.fullName

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}
}
// 똑같이 바인딩 됨
=============
KBS
54
KBS Kim
=============


또한 스프링부트에서는 프로퍼티 파일을 자동적으로 위 클래스 안에 선언한 변수의 타입에 맞추어 타입 변환(Type Conversion)이 일어나게 됩니다. 따라서 age 변수에 saelobi.age의 설정값이 자동적으로 바인딩되는 것이죠.


이 중에서 주목할 만한 타입 변환은 @DurationUnit을 통한 타입 변화입니다. 프로퍼티 파일에 시간과 관련된 설정값을 입력할 시 자동적으로 @DurationUnit에 지정한 시간 단위로 변환해줍니다.

# application.properties
saelobi.name=KBS
saelobi.age=${random.int(10,100)}
saelobi.full_name=${saelobi.name} Kim
# 25초
saelobi.sessionTimeout=25
@Component
@ConfigurationProperties("saelobi") // 인수로는 prefix 지정자를 받음
public class SaelobiProperties {

private String name; // prefix 지정자 때문에 saelobi.name 을 프로퍼티 파일에서 읽어옴

private int age; // saelobi.age

private String fullName; // saelobi.fullName

@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30); // 값이 안들어오면 기본값 30초

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}

public Duration getSessionTimeout() {
return sessionTimeout;
}

public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
}
=============
KBS
76
KBS Kim
PT25S
=============


| 설정값 검증 @Validated


스프링 부트에서는 @Validated 어노테이션을 통해 외부 설정값에 대한 검증을 자동적으로 수행할 수 있습니다.


다음은 saelobi.name을 값을 넣지 않았을 때 에러 메세지를 출력하는 것을 나타낸 것입니다. 이것은 @NotNull 어노테이션을 통해 검증할 수 있습니다. 이 밖에도 많은 어노테이션을 설정값을 받는 변수에 붙여서 다양한 검증을 할 수 있습니다.

# application.properties
saelobi.name=
#saelobi.name=KBS
saelobi.age=${random.int(10,100)}
saelobi.fullName=${saelobi.name} Kim
# 25초
saelobi.sessionTimeout=25
@Component
@ConfigurationProperties("saelobi") // 인수로는 prefix 지정자를 받음
@Validated
public class SaelobiProperties {

@NotEmpty
private String name; // prefix 지정자 때문에 saelobi.name 을 프로퍼티 파일에서 읽어옴

private int age; // saelobi.age

private String fullName; // saelobi.fullName

@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30); // 값이 안들어오면 기본값 30초

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}

public Duration getSessionTimeout() {
return sessionTimeout;
}

public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
}
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'saelobi' to com.tutorial.springboot.SaelobiProperties failed:

Property: saelobi.name
Value:
Origin: class path resource [application.properties]:3:0
Reason: 반드시 값이 존재하고 길이 혹은 크기가 0보다 커야 합니다.


참고자료 : https://www.inflearn.com/course/스프링부트


이 글을 공유하기

댓글(1)

Designed by JB FACTORY