[Spring Boot] 4. 스프링부트 외부설정

참조 : https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config

외부설정(External Config)

외부 설정이란 어플리케이션에서 사용하는 여러 설정 값들을
어플리케이션 밖(혹은 안)에 정의할 수 있는 기능을 말한다.

사용할 수 있는 외부 설정
  1. properties
  2. YAML
  3. 환경 변수
    tags:
  4. 커맨드 라인 아규먼트

properties

일단 백문이 불여일견. 예제를 통해 접해본다.

앞에서 살펴본 application.properties는 스프링 부트가
어플리케이션을 구동할때 자동으로 로딩하는 파일 이름이다.
클래스 경로에 이 이름의 속성 파일이 있으면 스프링 부트가
자동으로 해당 파일을 읽어준다.

속성 파일의 두는 곳은 몇가지 우선 순위가 존재한다.
(숫자가 낮을 수록 우선 순위가 높다.)

  1. 시작할 때 –spring.config.location에서 지정한 파일.
  2. 현재 디렉토리 바로 아래의 config 디렉토리에 있는 파일.
  3. 현재 디렉토리에있는 파일.
  4. 클래스 경로 바로 아래의 config 패키지에 있는 파일.
  5. 클래스 경로 바로 아래에 있는 파일.
    숫자가 낮을 수록 우선 순위가 높다.
application.properties
1
2
tester.name=GeunChang Ahn
tester.age=18
@Value어노테이션으로 참조
1
2
3
4
5
@Value("${tester.name}")
private String name;

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

다양한 방법으로 프로퍼티를 적용할 수 있기 때문에
우선순위가 존재한다.

프로퍼티 우선 순위

  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)

application.properties는 15번에 해당한다.
이전장에서 배운 아규먼트를 사용해서 테스트 해보자.
커맨드 라인 아규먼트는 4번 우선순위를 가지고 있다.

아규먼트를 주면 우선순위에 의해 아규먼트가 우선시 된다
1
java -jar target/springboot-applicationtest-0.0.1-SNAPSHOT.jar --user.name=Ahn

이렇게 하면 properties에 설정한 값과 다르게
아규먼트로 준 값으로 덮어쓰여진 것을 확인할 수 있다.

위에 @Value어노테이션 예제에서 ${tester.name}
${user.name}으로 바꾸면 현재 윈도우 계정의 이름이 찍힌다.
이는 OS환경변수가 10번 우선순위라 15번의 프로퍼티 파일보다
우선시 되기 때문이다.

테스트에선 2번,3번 우선 순위를 사용할 수 있다.
기본적으로 Environment를 통해서 가져올 수 있다.

SpringbootApplicationtestApplicationTests.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package me.rkaehdaos;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import org.springframework.test.context.junit4.SpringRunner;
//AssertJ
import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootApplicationtestApplicationTests {

@Autowired
Environment environment;

@Test
public void contextLoads(){
assertThat(environment.getProperty("tester.name"))
.isEqualTo("GeunChang Ahn");
}
}

위에서 environment에서 프로퍼티를 가져오는 부분을 잘 기억한다.

테스트 용도의 프로퍼티가 필요한 경우 어떻게 하면 좋을까
간단하게 테스트리소스에(src/test/resources) application.properties를
작성하면 된다.

테스트리소스의 application.properties
1
2
3
4
tester.name=tester
tester.age=${random.int(1,100)} #범위1~100, ','다음에 공백 없어야함
#먼저 선언한 변수는 재사용 가능
tester.fullname= ${tester.name} Ahn #placeholder

위 소스에서 test밑의 프로퍼티 값을 바꾸고 test를 실행하면
바꾼 프로퍼티값으로 셋 되기때문에 테스트를
통과하지 못하는 것을 볼 수 있다.

이는 컴파일 순서에 관한 것으로
일단 main에 있는 소스가 컴파일 되고 그다음에
test에 있는 것들이 컴파일되면서 클래스패스에 등록이 된다.
이때 테스트 용으로 덮어 씌여지는 것이다.

3번째 우선 순위를 사용해 본다.

1
2
3
@SpringBootTest(properties = "tester.name=3rd_Ahn")
public class SpringbootApplicationtestApplicationTests {
}

우선순위 3번째로 properties 애트리뷰트로 주어진 값이 우선시된다.
더 우선시 되는것은 별도의 TestPropertySource어노테이션이다.

1
2
3
4
@TestPropertySource(properties = "tester.name=2rd_Ahn" )
@SpringBootTest(properties = "tester.name=3rd_Ahn")
public class SpringbootApplicationtestApplicationTests {
}

@TestPropertySource는 우선순위 2번이므로 3번째를 덮어 쓴다.
value 애트리뷰트(value는 생략 가능)로 resource의 위치를 지정할 수 있다.

1
2
3
4
@TestPropertySource( "classpath:/com/example/test.properties")
@TestPropertySource( "file:/path/to/file.xml")
public class SpringbootApplicationtestApplicationTests {
}

앞에서처럼 application.properties를 소스와 테스트 2개를 관리할 경우
덮어 씌워지기때문에 소스쪽에서 100개가 필요하면 테스트에도 100개를 채워야한다.
이러면 둘다 관리가 어려워진다.(테스트에 필요한 것까지 해서 100개가 넘어갈 수도)

이 경우 위에서처럼 다른 이름으로 테스트에 프로퍼티파일(test.properties)
을 만들고 여기에 달라진 것들만 추가한후 @TestPropertySource로 테스트 프로퍼티
를 지정하면 어떻게 될까? 소스쪽이 컴파일 되서 들어가고 테스트쪽에서 프로퍼티
읽어드릴때 같은 키값의 프로퍼티는 테스트쪽으로 덮어쓰여지게 된다.
덮어 쓰여지는 이유는 위에서 봤다시피 jar 안의 properties파일은 15번 우선 순위이고
@TestPropertySource에 지정하면 2번 우선 순위가 되기 때문이다

application.properties 우선 순위

application.properties라는 같은 이름의 파일은 4군데에 놓을 수 있다.
만약 4군데에 다 놓는다면?
여기에도 우선순위가 있어서 높은것이 낮은것을 덮어 쓰게 된다.

  1. file:./config/
  2. file:./ (Root 혹은 Jar파일을 실행하는 위치)
  3. classpath:/config/
  4. classpath:/ (현재 리소스에 놓은 것은 4순위)

어차피 프로퍼티 우선순위 15번 안에서 싸움인 것을 인지하자.

타입-세이프 프로퍼티 @ConfigurationProperties

외부 설정이 많은 경우에 같은 키의 프로퍼티를 묶어서
하나의 빈으로 등록하는 방법이 있다.

1
2
3
4
5
6
7
8
9
10
package me.rkaehdaos;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("tester")
public class testerProperties {
String name; //tester.name 매칭
int age; //tester.age 매칭
... //getter, setter 생성 --BEAN 규칙에 따른다
}

저렇게 하면 인텔리J에서 경고가 뜬다.

Spring Boot Configuration annotation processor not found in classpath

application.properties를 ide로 작성하다보면
스프링 관련된 값이 자동 완성되는 것을 볼 수 있다.
이 자동완성은 메타 정보를 기반으로 하는데
이 메타정보를 완성해주는 플러그인을 추가하라는 알림이 뜨는 것이다.

spring-boot-configuration-processorjar를 사용함으로써
손쉽게 @ConfigurationProperties의 아이템들에 대한
고유 메타정보를 생성할 수 있다.

다음과 같이 의존성을 추가한다.

pom.xml
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

이제 빌드 할때 @ConfigurationProperties 메타정보를
생성해주어서 application.properties에서 해당 아이템에 대한
자동완성을 사용할 수 있다.

현재는 @ConfigurationProperties과 클래스 멤버 변수가
바인딩 되어 있을 뿐이며 아직은 사용할 수 없다.

원래는 다음과 같이 사용할 수 있으나

이해를 돕기위한 예제
1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
@EnableConfigurationProperties(testerProperties.class) // <--
public class SpringbootApplicationtestApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(SpringbootApplicationtestApplication.class);
app.addListeners(new SampleListener());
app.setWebApplicationType(WebApplicationType.NONE);
app.run(args);
}
}

@EnableConfigurationProperties 은 기본적으로 자동 등록 되어있다.
즉 어노테이션은 이미 설정되어 있는 상태니 생략해도 된다.
단지 properties클래스만 빈 설정을 하면 된다.
이렇게 빈등록이 되면 다른빈에 주입할 수가 있다.

TesterProperties.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package me.rkaehdaos;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component //TesterProperties를 빈 등록한다.
@ConfigurationProperties("tester")
public class TesterProperties {
private String name;
private int age;

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;
}
}
기존참조에서 프로퍼티클래스 사용으로 바꾼 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class AppArgumentTestComponent implements CommandLineRunner {

// 기존 참조 방법
/*
@Value("${tester.name}")
private String name;

@Value("${tester.age}")
private int age;
// */

// Properties 클래스 사용
@Autowired //빈 등록된 TesterProperties 인스턴스가 주입됨
TesterProperties testerProperties;

@Override
public void run(String... args) throws Exception {
//기존 출력
// System.out.println("name:"+name);
// System.out.println("age:"+age);
//
System.out.println("name:"+testerProperties.getName());
System.out.println("age:"+testerProperties.getAge());


}
}

이전의 @Value등으로 가져올때와의 차이는 무엇일까?

  1. 일단은 type safe하지 않다.
    @Value의 value에 오타를 칠 수도 있고 …
    하지만 이제 getter 메소드를 사용함으로써
    type safe할 수 있다.

  2. Meta-data 지원 여부
    위에서 봤듯이 @ConfigurationProperties 은 메타데이터를 지원함으로써
    application.properties(or yml)생성시 자동완성을 지원한다.

  3. Relaxed binding(융통성 있는 바인딩) 지원 여부

    참조 : https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-relaxed-binding

스프링 부트는 @ConfigurationProperties 빈에
Environment프로퍼티를 바인딩 할때 융통성 있는(Relaxed) 규칙을 적용해서
Environment프로퍼티 이름과 빈 프로퍼티 이름이 완벽하게 같지 않아도 된다.
대소문자 차이(PORT vs port),
dash(-)구분이랄지(context-path vs contextPath)등을 알아서 구분한다.
다음 4가지는 모두 동일하게 정상 작동하게 된다.

  • context-path : kebab-case (properties,yml에서 사용권장)
  • context_path : Underscore notation (properties,yml의 다른 포맷)
  • contextPath : Standard Camel case
  • CONTEXTPATH : Upper case (시스템 환경 변수에서 권장되는)
    @Value는 이걸 지원하지 않는다.
  1. SpEL 지원 여부
    @ConfigurationProperties 는 SpEL을 지원하지 않는다.
    @Value가 SpEL 표현을 지원하지만 프로퍼티 파일에서는 처리되지 않음을 명심하자.

properties 파일이 type safe 해봤자..

써드 파티 설정

참조: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-3rd-party-configuration

@ConfigurationProperties
@Component클래스에 사용했던 것처럼
public @Bean메소드에도 사용할 수 있다.

이는 개발자 컨트롤 밖의 써드 파티 컴포넌트들의
프로퍼티를 바인드 하고자 할때 유용하다.

Environment프로퍼티로 빈을 환경 설정하기 위해서
빈 등록에 @ConfigurationProperties를 추가한다.

1
2
3
4
5
6
//`@Bean`이 붙었으니 `@Component`를 붙일 순 없다.
@ConfigurationProperties(prefix = "another")
@Bean
public AnotherComponent anotherComponent() {
...
}
@Bean vs @Component
  • @Bean :
    메소드를 타겟으로 한다.
    메소드 반환 타입의 클래스의 인스턴스가가 빈이 된다.
    builder와 setter를 사용하여 사용자가 주고 싶은 값을 반영해서 생성된 인스턴스를
    스프링에게 관리하고 맡기는 것.
    class 소스를 접할 수 없는 라이브러리는 당연히 @Bean으로 밖에 접근할 수 없다.

  • @Component:
    클래스를 타겟으로 한다.
    사용자가 작성한 클래스 파일을 스프링에 맡겨서 스프링이 알아서
    이 클래스 타입의 인스턴스를 생성하여 빈 등록 한다.

이 차이를 알면 지금 이것도 알 수 있다.

프로퍼티 타입 컨버전

참조: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-conversion

스프링 부트는 외부 어플리케이션 프로퍼티를 @ConfigurationProperties bean들에게
바인딩 할때 알맞는 타입으로 컨버전하려고 시도한다.

프로퍼티는 사실 문서안에서는 타입이 없고 전부 문자열이다.
이를 스프링이 기본적으로 제공하는 컨버전 서비스를 통해서
타입 컨버전 되서 들어가는 것이다.

만약 커스텀한 타입 컨버전이 필요한 경우

  1. (conversionService 로 명명(named)된) conversionService 빈을 제공하거나
  2. (CustomEditorConfigurer빈을 통한) 커스텀 프로퍼티 에디터를 제공하거나
  3. (@ConfigurationPropertiesBinding를 빈 정의에 사용한)
    커스텀 Converters를 제공할 수 있다.

duration 컨버전

스프링 부트에는 이외에도 스프링 부트만이 제공하는 독특한 컨버전이 존재하는데
그 중 하나가 duration이다.

dur : 지속하다
during : 지속하는 사이
duration : 지속되는 기간, 존속 기간.

java.time.Duration의 Duration 클래스는 자바 8부터 존재하며 특정시간A와
특정시간 B 사이의 시간 차이를 초나 nano초의 시간의 양으로 모델링 한다.

java.time.Duration 프로퍼티를 노출(expose)하면은
어플리케이션 프로퍼티에서 다음과 같은 형식이 가능하다.

  • 일반적인 long 형식
    (@DurationUnit으로 단위 지정하지 않으면 기본단위로 밀리초)
  • java.util.Duration에 의해 사용되는 표준 ISO-8601 형식
  • value와 단위가 합쳐져서 사람이 더 읽기 쉬운 형식
    (ex. 10s = 10초)
위에서 작성했던 프로퍼티 클래스
1
2
3
4
5
6
7
8
9
@Component
@ConfigurationProperties("tester")
public class TesterProperties {

// 내용 스킵

@DurationUnit(ChronoUnit.SECONDS)
private Duration ssessionTimeout = Duration.ofSeconds(30);
}

이렇게 하면 프로퍼티에 ssessionTimeout이 없으면 기본값 30s를 가지게 되며
ssessionTimeout가 있으면 그값으로 주입이 된다.

application.properties
1
tester.ssessionTimeout=18

이렇게 하면 ide에서 경고가 뜬다 “Value ‘18 ‘ is not a valid duration”라며
18s18ns등의 입력을 권장하게 되지만 괜찮다.
위에서 어노테이션으로 단위를 SECONDS로 지정했기 되기때문에
18초로 들어가서 값을 출력해보면 PT18S로 18s 출력이 됨을 확인할 수 있다.

어노테이션을 쓰지 않고 단위 suffix를 붙일지
어노테이션을 쓰고 값만 취급할지는 상황에 따라서 활용이 가능할 것 같다.

고정된 프로퍼티라면 어노테이션 없이 쓰는 것이 좋을 것같고
숫자만 받아오는 상황이라면 단위처리를 위해 어노테이션을 써서 처리하는 것이 좋을 것
같다.

프로퍼티 값 데이터 검증 (Validate)

참조: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-validation

@ConfigurationProperties 클래스들이 스프링의 @Validated어노테이션이 붙으면
스프링 부트가 데이터 검증(validate)을 시도한다.

유효성 검사 ,데이터 검증 여러 말이 많지만 Validation으로 그냥 많이 쓰이기도..

스프링부트의 Validation 전에 기초부터 싹 정리


1. 자바의 Validation 스펙

만약 데이터검증이 프레젠테이션, 비지니스, 데이터 엑세스 각 레이어마다 거의 동일한
내용의 검증 로직이 구현된다면 그것은 중복이고 낭비가 심하며 각 계층 별 구현된
로직들의 불일치로 인하여 오류가 발생하기도 쉽다

이를 해결하기 위해 데이터 검증을 위한 로직을 도메인 모델 자체에 묶어서 표현하는
방법이 있다. 실제 코드로 표현되면 너무 장황하고 복잡하기에 자바에서는 Bean validation
이라는 어노테이션을 데이터 검증을 위한 메타데이터로 사용하는 방법을 제시한다

Bean Validation 명세는 현재 2.0 (JSR 380) 까지 나와있음

3. 스프링의 Validation

참조자료:

자바의 Bean validation 은 검사기(Validator)를 사용해야하고
그로부터 반환된 Set<ConstraintViolation<타입>>을 받아 사용해야 한다

Spring 에서는 Java Bean validation 을 완벽하게 지원하면서,
이런 Validator를 직접 사용하지 않고 AOP 와 같은 방식으로 더 편리하게
validation 할 수 있는 장치들을 제공하고 있다

자바의 validation @Valid를 사용해서 타겟을 지정했는데 이는
명세 1.1(JSR-349)에서 추가된 validation group의 어떤 그룹에서 validation이
일어날지 표현할 수 없다. validation group(검증 그룹)은 제약조건에 지정하는 그룹이다.
이를 이용해서 중복코드를 없애고 다른 검증방법을 선택할 수 있다.

예를 들어 일반 사용자 폼과 관리자용 폼이 같은 모델 클래스로 바인딩 되지만
다른방식의 Validation이 필요한다던지의 사례이다.

1
2
public interface User {}
public interface Admin {}

검증 그룹은 위처럼 내용없는 인터페이스로 정의한다. 일종의 마커 인터페이스

스프링에서는 검사기의 Validator.validate()를 사용해 유효성을 진행하지 않고
AOP같은 방법을 사용하기 때문에 어노테이션에 그룹세트를 명시적으로 지정해야 한다.
따라서 스프링은 @Validated라는 어노테이션을 따로 제공한다.

1
@Validated(PersonGroups.Driver.class)

@Validated대신 @Valid를 이용하면 groups로 지정한 검증 그룹은 무시되고
모든 제약조건이 일괄 적용된다.

스프링의 메소드에 대한 validation

AOP를 사용하여 메소드 실행시 validation을 한다.
MethodValidationPostProcessor 를 빈을 정의하고,
필요한 클래스 혹은 메소드에 @Validated 애노테이션을 추가하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}

@Validated
@Service
public class UserService {

public User addUser(@NotNull User user) {
...
}

...
}

제약 조건 위반시 ConstraintViolationException이 발생한다.

스프링 부트의 ValidationAutoConfiguration 을 보면 이미
MethodValidationPostProcessor빈이 정의 되어 있다.

Spring Validator

스프링 내부에서 사용하는 독자적인 Validator 인터페이스가 존재한다.
–> org.springframework.validation.Validator

Spring MVC의 Validation

데이터 바인딩 시점에 실행하게 된다.
WebMvcConfigurationSupportWebFluxConfigurationSupport를 보면
ConfigurableWebBindingInitializer생성시 Validator가 주입됨을 알 수 있다.


자 다시 돌아와서

위에서 봤던 @Validated@ConfigurationProperties 클래스에 사용이 가능하다.
@Validated와 함께 프로퍼티클래스에 제약조건 어노테이션을 달면 된다.

그리고 위의 스프링 메소드에서도 AOP기능으로 작동 되었듯이
써드파티의 @Bean빈에 @ConfigurationProperties 사용할때도 역시
@Validated를 사용하는 것이 가능하다.

지금까지 만들었던 프로퍼티에 제약조건을 붙였을때
1
2
3
4
5
6
@Component
@Validated
@ConfigurationProperties("tester")
public class TesterProperties {
@Size(min = 3, max = 15) ///이름 3~15크기
private String name;

프로퍼티의 이름을 위의 제약과 틀리게 하면 알파벳 하나만 넣자.
그럼 다음과 같은 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2018-10-31 15:04:48.380 ERROR 26696 --- [main] o.s.b.d.LoggingFailureAnalysisReporter   :

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'tester' to me.rkaehdaos.TesterProperties failed:

Property: tester.name
Value: G
Origin: class path resource [application.properties]:1:13
Reason: 반드시 최소값 3과(와) 최대값 15 사이의 크기이어야 합니다.

이렇게 어여쁜 에러메세지가 나올 수 있는 이유는 지난 번에 살펴봤던
FailureAnalyzer덕분이다.

정리

이렇게 프로퍼티를 밖에서 사용할때는 이렇게 하나의 키값으로 모아서
프로퍼티 클래스에서 validation까지 처리하자.이러면 해당 키값에 대한
자동완성까지 지원된다.

@Value보다 @ConfigurationProperties를 이용하는 것이 훨씬 좋다.

Related POST

공유하기