자바/스프링 부트

외부설정과 프로필2

UroJem 2023. 3. 26. 03:20

외부 설정 사용 - Environment

스프링이 제공하는 Environment를 통해 외부설정을 일관된 방식으로 조회할 수 있다.

 

외부 설정

  • 설정 데이터(application.properties)
  • OS 환경변수
  • 자바 시스템 속성
  • 커맨드 라인 옵션 인수

다양한 외부 설정 읽기

스프링은 Environment는 물론이고 Environment를 활용해서 더 편리하게 외부 설정을 읽는 방법들을 제공한다.

 

스프링이 지원하는 다양한 외부 설정 조회 방법

  • Environment
  • @Value - 값 주입
  • @ConfigurationProperties - 타입 안전한 설정 속성

외부 설정을 읽어서 활용하는 다양한 방법들을 알아본다.

예제에서는 가상의 데이터소스를 하나 만들고, 여기에 필요한 속성들을 외부 설정값으로 채운 다음 스프링 빈으로 등록하여 외부 설정값을 어떤식으로 활용하는지 이해를 돕기위해 만들었다. 실제 DB에 접근하지는 않는다.

외부 속성은 설정 데이터(application.properties)를 사용한다.

 

참고-properties 케밥 표기법

properties는 자바의 낙타 표기법(maxConnection)이 아니라 소문자와 '-'(dash)를 사용하는 케밥 표기법(max-connection)을 주로 사용한다. 참고로 이곳에 자바의 낙타 표기법을 사용한다고 해서 문제가 되는 것은 아니다. 스프링은 properties에 케밥 표기법을 권장한다.

 

MyDataSource에 값을 설정하고 스프링 빈으로 등록

실행결과

  • MyDataSouce를 스프링 빈으로 등록하는 설정 클래스이다.
  • Environment를 사용하면 외부 설정의 종류와 관계없이 코드 안에서 일관성 있게 외부 설정을 조회할 수 있다.
  • Environment.getProperty(key, Type)를 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다. (스프링 내부 변환기가 작동한다.)
    • env.getProperty는 인자값이 1개이면 해당 key값의 value를 기본 String 값으로 리턴한다.
    • env.getProperty("my.datasource.etc.max-connection", Integer.class) : 문자 -> 숫자로 변환
    • env.getProperty("my.datasource.etc.timeout", Duration.class) : 문자 -> Duration(기간) 변환
    • env.getProperty("my.datasource.etc.options", List.class) : 문자 -> List 변환 (A,B -> [A,B])

스프링은 다양한 타입들에 대해서 기본 변환 기능을 제공한다.

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties.conversion.durations

Duration 클래스는 자바 8에 추가된 Time 패키지에 추가된 기능으로 두 시간 사이의 간격을 초나 나노 초 단위로 나타내는 기능이다.
비슷한 기능으로 Period 라는 클래스는 두 날짜 사이의 간격을 나타내는 클래스이다.

Duration의 between 메서드에 시작 시간과 종료시간을 넘기면 두 시간 사이의 간격을 나타내는 Duration 객체를 생성한다.
또한 ofXxx 정적 메서드를 사용하면 Duration 클래스를 생성할 수 있다.
Duration 클래스를 통해 제어할 수 있는 가장 큰 단위는 "일" 이다. 그보다 큰 단위는 Period 클래스를 이용한다.
*참고
https://www.daleseo.com/java8-duration-period/

스프링 타입 변환기 기능으로 3500ms 라고 적힌 외부 설정값이 3.5초의 값을 가진 Duration 클래스 객체로 변환되고 

CACHE,ADMIN 라고 적힌 값이 [CACHE, ADMIN] List로 변환이 된 것 같다.

 

  • 설정 정보를 빈으로 등록해서 사용하기 위해 @Import(MyDataSourceEnvConfig.class)를 메인 메서드 돌리는 데에서 추가했다. 컴포넌트 스캔 대상의 패키지에서 빼버렸기 때문
  • @SpringBootApplication(scanBasePackages = "hello.datasource")
    • 예제에서는 @Import 로 설정 정보를 계속 변경할 예정이므로, 설정 정보를 바꾸면서 사용하기 위해 hello.config의 위치를 피해서 컴포넌트 스캔 위치를 정했다.
    • scanBasePackages 설정을 하지 않으면 현재 위치인 hello 패키지 부터 그 하위가 모두 컴포넌트 스캔 대상이되어 @Configuration을 포함하고 있는 MyDataSourceEnvConfig 가 항상 컴포넌트 스캔의 대상이 된다. 설정 클래스를 계속 추가하여 바꾸면서 사용할 예정이라 이전 설정 정보가 스캔되어 동작할 수 있으니 스캔하지 않도록 대상 패키지를 빼버린 것

.application.properties에 필요한 외부 설정을 추가하고 Environment를 통해서 해당 값들을 읽어서 MyDataSource를 만들었다. 향후 외부 설정 방식이 달라져도, 예를 들어 설정데이터(application.properties)를 사용하다가 커맨드라인 옵션 인수나 자바 시스템 속성으로 변경해도 애플리케이션 코드를 그대로 유지할 수 있다. Envirinment가 다 추상화하여 기능을 가지고 있기 때문에

 

이방식의 단점은 Environment를 직접 주입받고 env.getProperty(key)를 통해서 값을 꺼내는 과정을 반복해야 한다는 점이다. 스프링은 @Value를 통해서 외부 설정값을 주입받는 더욱 편리한 기능을 제공한다.

 

 

외부설정 사용 - @Value

@Value를 사용하면 외부 설정값을 편리하게 주입받을 수 있다.

@Value도 내부에서는 Environment를 사용한다.

  • @Value에 ${}를 사용해서 외부 설정의 키 값을 주면 원하는 값을 주입받을 수 있다.
  • @Value는 필드에 사용할 수도 있고, 파라미터에 사용할 수도 있다.
    • myDataSource1() 는 필드에 주입받은 설정값을 사용한다.
    • myDataSource2() 는 파라미터를 통해서 설정값을 주입 받는다.
    • 만약 키를 찾지 못할 경우 코드에서 기본값을 사용하려면 다음과 같이 : 뒤에 기본값을 적어주면 된다.
      • 예 @Value("${my.datasource.etc.max-connection:1}") : key가 없는 경우 1을 기본값으로 사용한다.

기존에 사용했던 설정 객체인 MyDataSourceEnvConfig.class 를 주석 처리하고 새로 만든 MyDataSourceValueConfig.class를 Import 해준다.

실행 결과

필드로 주입받은 설정값과 파라미터로 주입받은 설정값 2개를 설정해서 같은 실행결과가 두번 나온다.

 

application.properties에 필요한 외부 설정을추가하고 @Value를 통해서 해당 값들을 읽어서 MyDataSource를 만들었다.

 

@Value를 사용하는 방식도 좋지만 @Value로 하나하나 외부 설정 정보의 키 값을 입력받고, 주입 받아와야 하는 부분이 번거롭다. 그리고 설정 데이터를 보면 하나하나 분리되어 있는 것이 아니라 정보의 묶음으로 되어있다.

여기서는 my.datasource 부분으로 묶여있다. 이런 부분을 객체로 변환해서 사용할 수 있다면 더 편리하고 좋을 것이다.

 

 

외부설정 사용 - @ConfigurationProperties 시작

Type-safe Configuration Properties 

스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다. 이것을 타입 안전한 설정 속성이라 한다.

객체를 사용하면 타입을 사용할 수 있다. 따라서 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있고, 객체를 통해서 활용할 수 있는 부분들이 많아진다. 쉽게 이야기해서 외부 설정을 자바 코드로 관리할 수 있는 것이다.

그리고 설정 정보 그 자체도 타입을 가지게 된다.

경고가 뜨지만 일단 무시하고

  • 외부 설정을 주입받을 객체를 생성한다. 그리고 각 필드를 외부 설정의 키 값에 맞추어 준비한다.
  • @ConfigurationProperties가 있으면 외부 설정을 주입 받는 객체라는 뜻이다. 여기에 외부 설정 key의 묶음 시작점인 my.datasource를 적어준다.
  • 기본 주입 방식은 자바빈 프로퍼티 방식이다. Getter, Setter가 필요하다. (롬복의 @Data에 의해 자동 생성된다.)

  • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
    • 이 애노테이션은 @ConfigurationProperties를 적용한 클래스(MyDataSourcePropertiesV1.class)를 속성값으로 주어지면 해당 클래스를  스프링 빈으로 등록하고 프로퍼티 값을 할당하는 일을 한다. 빈으로 등록되었으니 필요한 곳에서 주입 받아서 사용할 수 있다.
    • private final MyDataSourcePropertiesV1 properties 설정 속성을 생성자를 통해 주입 받아서 사용한다.

기존에 사용했던 설정 객체인 MyDataSourceValueConfig.class 를 주석 처리하고 새로 만든 MyDataSourceConfigV1.class를 Import 해준다.

잘 출력이 된다.

타입 안전

ConfigurationProperties를 사용하면 타입 안전한 설정 속성을 사용할 수 있다.

int 타입으로 받은 maxConnection 값을 만약 String 타입으로 변경할 경우 객체로 바인딩할 때 타입이 달라서 NumberFormatException이 발생한다. 실수로 숫자를 입력하는 곳에 문자를 입력하는 문제를 방지해줘서 타입에 안전한 속성이라고 한다. ConfigurationProperties로 만든 외부 데이터는 타입에 대해서 믿고 사용할 수 있다.

 

정리

application.properties에 필요한 외부설정을 추가하고 @ConfigurationProperties를 통해서 MyDataSourcePropertiesV1에 외부 설정의 값들을 설정했다. 그리고 해당 값들을 읽어서 MyDataSource를 만들었다. 비단 application.properties 파일 정보만 읽어올 수 있는게 아니라 Environment로 받을 수 있는 모든 외부 설정들을 ConfigurationProperties를 통해 받아올 수 있다.

 

표기법 변환

스프링은 외부설정에 케밥 케이스, 스네이크 케이스, 파스칼 케이스로 키값을 설정해도 카멜 케이스로 중간에서 자동으로 변환하여 바인딩해준다. 당연히 같은 카멜케이스를 사용해도 상관은 없으나 관례상 외부 설정에는 케밥 케이스로 작성한다.

max-connection / max_connection / MaxConnection -> maxConnection 

 

@ConfigurationPropertiesScan

  • @ConfigurationProperties 를 하나하나 직접 등록할 때는 @EnableConfigurationProperties를 사용한다.
    • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
  • @ComponentScan 이 @Component를 가진 클래스라면 빈으로 등록되는 것처럼 @ConfigurationPropertiesScan을 사용하면 @ConfigurationProperties를 가진 클래스를 빈으로 등록해준다.

기존의 등록 방법을 가리고

스프링 부트를 시작하는 메인 메서드가 있는 곳에 @ConfigurationPropertiesScan({"hello"}) 작성해준다. 속성 값은 해당 패키지 부터 @ConfigurationProperties를 가진 클래스를 빈으로 등록해준다.

 

우리가 실무에서 사용하는 외부 설정들도 어디엔가 이런 작업들이 쓰여지고 있어서 사용할 수 있다.

 

문제

MyDataSourcePropertiesV1은 스프링 빈으로 등록된다. 그런데 Setter 메서드를 가지고 있기 때문에 누군가가 중간에서 실수로 값을 변경하는 문제가 발생할 수 있다. 여기에 있는 값들은 외부 설정값을 사용해서 초기에만 설정되고, 이후에는 변경하면 안된다. 이럴 때 Setter 메서드를 제거하고 대신에 생성자를 사용하면 중간에 데이터를 변경하는 실수를 근본적으로 방지할 수 있다.

이런 문제가 없을 것 같지만 한번 발생하면 정말 잡기 어려운 버그가 만들어진다.

대부분의 개발자가 MyDataSourcePropertiesV1의 값은 변경하면 안된다고 인지하고 있지만, 어떤 개발자가 자신의 문제를 해결하기 위해 Setter 메서드를 통해 값을 변경하게 되면 애플리케이션 전체에 심각한 버그를 유발할 수 있다.

좋은 프로그램은 제약이 있는 프로그램이다.

 

 

외부설정 사용 - @ConfigurationProperties 생성자

@ConfigurationProperties는 Getter, Setter 를 사용하는 자바 빈 프로퍼티 방식이 아니라 생성자를 통해서 객체를 만드는 기능도 지원한다.

기존에 V1을 V2로 config 파일, datasource 파일 복사한 후 메인메서드가 있는 객체에 @Import 속성을 V2로 변경한다.
값이 잘 주입됨

  • 기존의 setter메서드 주입방식에서 생성자 주입방식으로 변경한다. 빈 생성시 초기값 세팅을 통해 처음 설정된 정보로 properties 객체가 만들어진다.
  • 기존 @Data 를 빼고 값만 가져올 수 있게 @Getter 롬복을 사용하여 자동으로 getter 메서드를 만들어 준다.
  • @DefaultValue : 해당 값을 찾을 수 없는 경우 기본값을 사용하는 애노테이션
    • @DefaultValue Etc etc : etc를 찾을 수 없을 경우 Etc 객체를 생성하고 내부에 들어가는 값은 초기화 메커니즘에 의해 정수는 0, 실수는 0.0, 논리형은 false, 문자형은 \u0000, 참조형은 null이 담긴다.
    • @DefaultValue("DEFAULT") List<String> options : options를 찾을 수 없을 경우 DEFAULT 값이 담긴 리스트가 만들어진다.

Etc 객체에 들어가는 값을 주석 처리하고
default 값이 설정 됐다.

참고

@ConstructorBinding

스프링 3.0 이전에는 생성자 주입 사용시 생성자에 @ConstructorBinding 애노테이션을 필수로 사용해야 했지만

스프링 3.0 이후 생성자가 하나일 때는 생략가능하고 생성자가 2이상인 경우에는 사용할 생성자에 @ConstuctorBinding 애노테이션을 적용하면 된다.

 

정리

application.properties에 필요한 외부 설정을 추가하고 @ConfigurationProperties의 생성자 주입을 통해서 값을 읽어들였다. Setter 메서드가 없으므로 개발자가 중간에 실수로 값을 변경하는 문제가 발생하지 않는다.

 

문제

타입과 객체를 통해서 숫자에 문자가 들어오는 것 같은 기본적인 타입 문제들은 해결되었지만 타입은 맞는데 숫자의 범위가 기대하는 것과 다르다면?

예를 들어 max-connection의 값을 0으로 설정하면 커넥션이 하나도 만들어지지 않는 심각한 문제가 발생한다고 치자.

max-connection은 최소 1이상으로 설정하지 않으면 애플리케이션 로딩 시점에 예외를 발생시켜서 빠르게 문제를 인지할 수 있도록 하고 싶다.

 

 

외부설정 사용 - @ConfigurationProperties 검증

@ConfigurationProperties를 통해 타입 안전성은 체크가 되지만 가져오는 값에 사용할 수 없는 값이 들어온다면?

커넥션의 숫자는 최소 1개 이상이여야 하는데 0이나 음수값이 들어온다던가 이메일이나 전화번호의 값을 받아야 하는 설정에 형식에 맞지 않는 값이 들어온다면 타입통과는 되지만 사용할 수 없는 값이 들어온다. 추후 서비스 이용중 오류가 위험이 존재한다.

이를 방지하기 위해 개발자가 직접 하나하나 검증 코드를 작성해도 되지만 자바에는 자바 빈 검증기(java bean validation)이라는 훌륭한 표준 검증기가 제공된다.

 

@ConfigurationProperties는 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원한다.

 

자바 빈 검증기를 사용하려면 spring-boot-starter-validation이 필요하다.

build.gradle에 validation 추가후 refresh
MyDataSourcePropertiesV2파일을 MyDataSourcePropertiesV3로 복사한다

  • @NotEmpty 애노테이션이 붙은 필드값은 항상 값이 있어야 한다. 필수 값이 된다.
  • @Min(1) @Max(999) maxConnection : 최소 1, 최대 999의 값을 허용한다.
  • @DurationMin(second = 1) @DurationMax(second = 60) : 최소 1초, 최대 60초를 허용한다.

jakarta.validation.constraints.Max

패키지 이름에 jakarta.validation 으로 시작하는 것은 자바 표준 검증기에서 지원하는 기능이다.

 

org.hibernate.validator.constraints.time.DurationMax

패키지 이름에 org.hibernate.validator 로 시작하는 것은 자바 표준 검증기에서 아직 표준화된 기능은 아니고 하이버네이트 검증기라는 표준 검증기위 구현체에서 직접 제공하는 기능이다. 대부분 하이버네이트 검증기를 사용하므로 크게 문제가 되지 않는다.

 

MyDataSourceConfigV2를 복사하여 MyDataSourceConfigV3 만들고
ExternalReadApplication 코드 Import를 MyDataSourceConfigV3.class로 변경하고 실행한다.
실행 잘됨

외부 설정 값을 범위 밖의 값으로 변경한다면?

범위 값을 알려주며 에러가 난다.

 

정리

ConfigurationProperties 덕분에 타입에 안전하고, 또 매우 편리하게 외부 설정을 사용할 수 있다. 그리고 검증기 덕분에 쉽고 편리하게 설정 정보를 검증할 수 있다.

가장 좋은 예외는 컴파일 예외, 그리고 애플리케이션 로딩 시점에 발생하는 예외이다. 가장 나쁜 예외는 고객 서비스 중에 발생하는 런타임 예외이다.

 

ConfigurationProperties 장점

  • 외부 설정을 객체로 편리하게 변환해서 사용할 수 있다.
  • 외부 설정의 계층을 객체로 편리하게 표현할 수 있다.
  • 외부 설정을 타입 안전하게 사용할 수 있다.
  • 검증기를 적용할 수 있다.

 

YAML

스프링은 설정 데이터 사용시 application.properties 파일뿐 아니라 application.yml 파일 형식도 지원한다.

YAML(YAML Ain't Markup Language) 은 사람이 읽기 좋은 데이터 구조를 목표로 한다. 확장자는 yaml, yml이다. 주로 yml을 사용한다.

  • YAML의 가장 큰 특징은 사람이 읽기 좋게 계층 구조를 이룬다는 점이다.
  • YAML은 space(공백)로 계층 구조를 만든다. space는 1칸을 사용해도 되는데, 보통 2칸을 사용한다. 일관성 있게 사용하지 않으면 읽기 어렵거나 구조가 깨질 수 있다.
  • 구분 기호로 : 를 사용한다. 만약 값이 있다면 이렇게 key: value 형식으로 : 이후에 공백을 하나 넣고 값을 넣어주면 된다.

스프링은 YAML 파일을 읽어들일 때 계층 구조를 properties 처럼 평평하게 만들어서 읽어들인다. 쉽게 이야기 해서 application.yml 형식이 application.properties 형식으로 변경되어 스프링에서 사용한다.

기존의 application.properties 파일은 백업파일로 변경하고 application.yml 파일을 만든다.

실행하면 application.yml 파일에 입력한 설정 데이터가 조회되는 것을 확인할 수 있다.

 

주의

application.properties, application.yml 파일을 같이 사용하면 application.properties 파일이 우선권을 가진다. 

참고로 실무에서는 설정 정보가 많아서 보기 편한 yml을 선호한다.

 

 

YML과 프로필

yml 파일로 프로필 별 외부 설정 작성
실행시 prod 프로필로 실행
출력 잘됨

  • yml은 --- 대시 3개를 사용해서 논리 파일을 구분한다.
  • spring.config.active.on-profile을 사용해서 프로필을 적용할 수 있다.
  • 나머지는 application.properties와 동일하다.

 

@Profile

프로필과 외부설정을 사용해서 각 환경마다 설정값을 다르게 적용할 수 있지만 프로필 마다 서로 다른 빈을 등록해야 하는 경우가 생기면?

개발 환경에서는 결제 기능이 실제로 발생되면 안되고 문자 발송기능 스케쥴링 기능이 발생되면 문제가 발생하니 가짜 스프링 빈을 등록하고 운영환경에서는 실제 동작하는 스프링 빈이 등록되어야 한다.

 

hello 패키지 밑에 pay 패키지 생성 후 로컬 결제기능, 운영 결제기능 DI 활용을 위해 인터페이스로 PayClient를 만든다.

PayClient를 구현한 LocalPayClient를 만들고 로컬 개발 환경에서는 실제 결제를 하지 않는 로직이 들어간다고 생각하면 된다.

PayClient를 구현한 ProdPayClient를 만들고 운영 개발 환경에서는 실제 결제를 하는 로직이 들어간다고 생각하면 된다.

PayClient를 사용하는 서비스 부분이다. 상황에 따라 LocalPayClient 또는 ProdPayClient를 주입받는다.

@RequiredArgsConstructor : 롬복을 사용하여 final이 붙거나 @NotNull이 붙은 필드의 생성자를 자동 생성해주는 어노테이션이다.

  • @Profile 애노테이션을 사용하면 해당 프로필이 활성화된 경우에만 빈을 등록한다.
    • default 프로필(기본값)이 활성화 되어 있으면 LocalPayClient를 빈으로 등록한다.
    • prod 프로필이 활성화 되어 있으면 ProdPayClient를 빈으로 등록한다.

ApplicationRunner 인터페이스를 사용하면 스프링 빈은 초기화가 모두 끝나고 애플리케이션 로딩이 완료되는 시점에 run(args) 메서드를 호출한다.

스프링 부트가 로딩될 때 컴포넌트 스캔으로 OrderRunner가 빈 등록될 때 @RequireArgsConstructor로 인해 OrderService를 주입받으며 OrderRunner가 생성되고 추후 로딩이 완료되는 시점에 run 메서드가 실행되어 orderService.order(1000); 메서드가 실행되는 것.

 

ApplicationRunner : SpringBootApplication이 포함된 프로그램에서 특정 Bean을 Application 실행 후 실행하도록 하는 인터페이스로 메소드는 run이라는 메소드 뿐이며 ApplicationArguments를 받도록 되어있다. 사용방법은 실행하고자 하는 클래스를 ApplicationRunner 인터페이스를 구현하면 된다.

컴포넌트 스캔 범위를 hello.pay 패키지도 포함 시킨다.

프로필이 없다면 default 프로필이 사용되어 LocalPayClient가 빈으로 등록 된다.

prod 프로필을 적용하면 ProdPayClient가 빈으로 등록 된다.

 

@Profile의 정체

@Profile은 특정 조건에 따라서 해당 빈을 등록할지 말지 선택하는 애노테이션으로 @Conditional 애노테이션을 사용한다.

@Profile의 속성값을 가져와서 Environment 외부 설정값의 프로필에 해당 속성값이 있다면 매칭되는 속성값의 @Profile 애노테이션이 달린 메서드를 실행시키는 것.

스프링은 @Conditional 기능을 활용해서 개발자가 더 편리하게 사용할수 있는 @Profile 기능을 제공한다.

 

정리

@Profile을 사용하면 각 환경 별로 외부 설정 값을 분리하는 것을 넘어서, 등록되는 스프링 빈도 분리할 수 있다.

 

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9/dashboard