[spring boot] 외부 설정과 프로필2

현서황·2024년 10월 14일

스프링이 지원하는 다양한 외부 설정 조회 방법
Environment
@Value - 값 주입
@ConfigurationProperties - 타입 안전한 설정 속성

이 세가지는 밑으로 갈 수록 더욱 편리하다!


외부 설정 사용 - Environment


MyDataSource

@Slf4j
@Data
public class MyDataSource {
 private String url;
 private String username;
 private String password;
 private int maxConnection;
 private Duration timeout;
 private List<String> options;
 public MyDataSource(String url, String username, String password, int
maxConnection, Duration timeout, List<String> options) {
 this.url = url;
 this.username = username;
 this.password = password;
this.maxConnection = maxConnection;
 this.timeout = timeout;
 this.options = options;
 }
 @PostConstruct
 public void init() {
 log.info("url={}", url);
 log.info("username={}", username);
 log.info("password={}", password);
 log.info("maxConnection={}", maxConnection);
 log.info("timeout={}", timeout);
 log.info("options={}", options);
 }
}

MyDataSourceEnvConfig

@Slf4j
@Configuration
public class MyDataSourceEnvConfig {

 	private final Environment env;
    
 	public MyDataSourceEnvConfig(Environment env) {
 		this.env = env;
 	} //여기에서 bean으로 주입됨!
    
 	@Bean
 	public MyDataSource myDataSource() {
 		String url = env.getProperty("my.datasource.url");
 		String username = env.getProperty("my.datasource.username");
 		String password = env.getProperty("my.datasource.password");
 		int maxConnection = env.getProperty("my.datasource.etc.max-connection", Integer.class);
 		Duration timeout = env.getProperty("my.datasource.etc.timeout",Duration.class);
 		List<String> options = env.getProperty("my.datasource.etc.options",List.class);
 		
        return new MyDataSource(url, username, password, maxConnection, timeout,options);
 	}
}

MyDataSource를 스프링 빈으로 등록하는 자바 설정이다.
Environment를 사용하면 외부 설정의 종류와 관계없이 코드 안에서 일관성 있게 외부 설정을 조회할 수 있다.
Environment.getProperty(key,Type)을 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다.(스프링 내부 변환기가 작동한다.)
- env.getProperty("my.database.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] )

ExternalReadApplication

@Import(MyDataSourceEnvConfig.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
	public static void main(String[] args) {
		SpringApplication.run(ExternalReadApplication.class, args);
	}
}
  • 설정 정보를 빈으로 등록해서 사용하기 위해 @Import(MyDataSourceEnvConfig.class)를 추가했다.
  • @SpringBootApplication(scanBasePackages = "hello.database")
    • 예제에서는 @Import 로 설정 정보를 계속 변경할 예정이므로, 설정 정보를 바꾸면서 사용하기 위해 hello.config 의 위치를 피해서 컴포넌트 스캔 위치를 설정했다.
    • scanBasePackages 설정을 하지 않으면 현재 위치인 hello 패키지부터 그 하위가 모두 컴포넌트 스캔이 된다. 따라서 @Configuration 을 포함하고 있는 MyDataSourceEnvConfig 이 항상 컴포넌트 스캔의 대상이 된다.

외부설정 사용 - @Value

@Value를 사용하면 외부 설정값을 편리하게 주입받을 수 있다.
참고로 @Value도 내부에서는 Environment를 사용한다.

@Slf4j
@Configuration
public class MyDataSourceValueConfig {
 	@Value("${my.datasource.url}")
 	private String url;
 	@Value("${my.datasource.username}")
 	private String username;
 	@Value("${my.datasource.password}")
 	private String password;
 	@Value("${my.datasource.etc.max-connection}")
 	private int maxConnection;
 	@Value("${my.datasource.etc.timeout}")
 	private Duration timeout;
 	@Value("${my.datasource.etc.options}")
 	private List<String> options;
 
 	@Bean
 	public MyDataSource myDataSource1() {
 		return new MyDataSource(url, username, password, maxConnection, timeout,options);
 	}
 
 	@Bean
 	public MyDataSource myDataSource2(
 @Value("${my.datasource.url}") String url,
 @Value("${my.datasource.username}") String username,
 @Value("${my.datasource.password}") String password,
 @Value("${my.datasource.etc.max-connection}") int maxConnection,
 @Value("${my.datasource.etc.timeout}") Duration timeout,
 @Value("${my.datasource.etc.options}") List<String> options) 		{
 	return new MyDataSource(url, username, password, maxConnection, timeout,options);
 	}
}

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

ExternalReadApplication - 수정

//@Import(MyDataSourceEnvConfig.class)
@Import(MyDataSourceValueConfig.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {
	 public static void main(String[] args) {
	 SpringApplication.run(ExternalReadApplication.class, args);
 	}
}
  • @Import(MyDataSourceEnvConfig.class) 를 주석처리 한다.
  • @Import(MyDataSourceValueConfig.class) 를 추가 한다.

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

단점
@Value 를 사용하는 방식도 좋지만, @Value 로 하나하나 외부 설정 정보의 키 값을 입력받고, 주입 받아와야 하는 부분이 번거롭다. 그리고 설정 데이터를 보면 하나하나 분리되어 있는 것이 아니라 정보의 묶음으로 되어 있다. 여기서는 my.datasource 부분으로 묶여있다. 이런 부분을 객체로 변환해서 사용할 수 있다면 더 편리하고 더 좋을 것이다.


외부 설정 사용 - @ConfigurationProperties 시작

Type-safe Configuration Properties
스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다. 이것을 타입 안전한 설정 속성이라 한다.
객체를 사용하면 타입을 사용할 수 있다. 따라서 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있고, 객체를 통해서 활용할 수 있는 부분들이 많아진다. 쉽게 이야기해서 외부 설정을 자바 코드로 관리할 수 있는 것이다. 그리고 설정 정보 그 자체도 타입을 가지게 된다.

MyDataSourcePropertiesV1

@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {
	private String url;
	private String username;
	private String password;
	private Etc etc = new Etc();
 
	@Data
 	public static class Etc {
		private int maxConnection;
 		private Duration timeout;
		private List<String> options = new ArrayList<>();
 	}
}
  • 외부 설정을 주입받을 객체를 생성한다. 그리고 각 필드를 외부 설정의 키 값에 맞추어 준비한다.
  • @ConfigurationProperties가 있으면 외부 설정을 주입받는 객체라는 뜻이다. 여기에 외부 설정의 KEY묶음 시작점인 my.datasource를 적어준다.
  • 기본 주입 방식은 자바빈 프로퍼티 방식이다. Getter, Setter가 필요하다.(롬복의 @Data에 의해 자동 생성된다.)

빈 프로퍼티 방식(Property Injection)
빈 프로퍼티 주입은 자바에서 객체의 의존성을 외부에서 주입할 때 사용되는 기법이다. 이 방식에서는 특정 필드(프로퍼티)에 대해 Setter 메서드를 통해 의존성을 주입하는 것이 일반적이다.
예를 들어, 어떤 클래스가 외부 객체나 리소스에 의존할 때 그 의존성을 생성자가 아닌 Setter 메서드를 통해 설정해주는 것이 이 방식의 핵심이다.
코드에서 Setter 메서드를 정의해놓으면 Spring 컨테이너가 객체 생성 후 해당 메서드를 통해 의존성을 주입하게 된다.

설정 속성을 어떻게 사용하냐면 ~

MyDataSourceConfigV1

import
org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
public class MyDataSourceConfigV1 {
 	private final MyDataSourcePropertiesV1 properties;
 	public MyDataSourceConfigV1(MyDataSourcePropertiesV1 properties) {
 		this.properties = properties;
 	}
 	@Bean
 	public MyDataSource dataSource() {
 		return new MyDataSource(
 			properties.getUrl(),
 			properties.getUsername(),
 			properties.getPassword(),
			properties.getEtc().getMaxConnection(),
			properties.getEtc().getTimeout(),
			properties.getEtc().getOptions());
	 }
}
  • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
    • 스프링에게 사용할 @ConfigurationProperties 를 지정해주어야 한다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있다.
  • private final MyDataSourcePropertiesV1 properties 설정 속성을 생성자를 통해 주입 받아서 사용한다.

ExternalReadApplication - 수정

//@Import(MyDataSourceValueConfig.class)
@Import(MyDataSourceConfigV1.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {...}
  • @Import(MyDataSourceValueConfig.class)를 주석처리한다.
  • @Import(MyDataSourceConfigV1.class)를 추가한다.

정리
application.properties 에 필요한 외부 설정을 추가하고, @ConfigurationProperties 를 통해서 MyDataSourcePropertiesV1 에 외부 설정의 값들을 설정했다. 그리고 해당 값들을 읽어서 MyDataSource 를 만들었다.

표기법 변환
maxConnection 은 표기법이 서로 다르다. 스프링은 캐밥 표기법을 자바 낙타 표기법으로 중간에서 자동으로 변환해준다.

  • application.properties 에서는 max-connection
  • 자바 코드에서는 maxConnection

@ConfigurationPropertiesScan

  • @ConfigurationProperties 를 하나하나 직접 등록할 때는
    @EnableConfigurationProperties 를 사용한다.
    - @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
  • @ConfigurationProperties 를 특정 범위로 자동 등록할 때는 @ConfigurationPropertiesScan 을 사용하면 된다.

@ConfigurationPropertiesScan 예시

@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyApplication {}

빈을 직접 등록하는 것과 컴포넌트 스캔을 사용하는 차이와 비슷하다.

문제
MyDataSourcePropertiesV1 은 스프링 빈으로 등록된다. 그런데 Setter 를 가지고 있기 때문에 누군가 실수로 값을 변경하는 문제가 발생할 수 있다. 여기에 있는 값들은 외부 설정값을 사용해서 초기에만 설정되고, 이후에는 변경 하면 안된다. 이럴 때 Setter 를 제거하고 대신에 생성자를 사용하면 중간에 데이터를 변경하는 실수를 근본적으로 방지할 수 있다.
이런 문제가 없을 것 같지만, 한번 발생하면 정말 잡기 어려운 버그가 만들어진다.
대부분의 개발자가 MyDataSourcePropertiesV1 의 값은 변경하면 안된다고 인지하고 있지만, 어떤 개발자가 자신의 문제를 해결하기 위해 setter 를 통해서 값을 변경하게 되면, 애플리케이션 전체에 심각한 버그를 유발할 수 있다. 좋은 프로그램은 제약이 있는 프로그램이다.

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

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

MyDataSourcePropertiesV2

@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {
 	private String url;
 	private String username;
 	private String password;
 	private Etc etc;
 	
    //이게 생성자! Setter 대신!
    public MyDataSourcePropertiesV2(String url, String username, String password, @DefaultValue Etc etc) {
 		this.url = url;
 		this.username = username;
 		this.password = password;
 		this.etc = etc;
 	}
 
 	@Getter
 	public static class Etc {
 		private int maxConnection;
 		private Duration timeout;
		private List<String> options;
 		public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
 			this.maxConnection = maxConnection;
 			this.timeout = timeout;
 			this.options = options;
 		}
 	}
}
  • 생성자를 만들어 두면 생성자를 통해서 설정 정보를 주입한다.
  • @Getter 롬복이 자동으로 getter 를 만들어준다.
  • @DefaultValue : 해당 값을 찾을 수 없는 경우 기본값을 사용한다.
    • @DefaultValue Etc etc
      • etc 를 찾을 수 없을 경우 Etc 객체를 생성하고 내부에 들어가는 값은 비워둔다. ( null , 0 )
    • @DefaultValue("DEFAULT") List options
  • options 를 찾을 수 없을 경우 DEFAULT 라는 이름의 값을 사용한다.

참고 @ConstructorBinding
스프링부트 3.0이전에는 생성자 바인딩 시에 @ConstructorBinding 애노테이션을 필수로 사용해야 했다.
스프링부트 3.0 부터는 생성자가 하나일 때는 생략할 수 있다. 생성자가 둘 이상인 경우에는 사용할 생성자에 @ConstructorBinding어노테이션을 적용하면 된다.

Setter 없이 생성자 주입을 이용해 만든 MyDataSourcePropertiesV2 를 사용해보자.

@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV2.class)
public class MyDataSourceConfigV2 {
	private final MyDataSourcePropertiesV2 properties;

	public MyDataSourceConfigV2(MyDataSourcePropertiesV2 properties) {
		this.properties = properties;
	}
	
@Bean
	public MyDataSource dataSource() {
		return new MyDataSource(
			properties.getUrl(),
			properties.getUsername(),
			properties.getPassword(),
			properties.getEtc().getMaxConnection(),
			properties.getEtc().getTimeout(),
			properties.getEtc().getOptions());
	}
}
  • MyDataSourcePropertiesV2 를 적용하고 빈을 등록한다. 기존 코드와 크게 다르지 않다.

생성자 주입 방식은 Setter 없이 생성자를 만들어놓으면 되는 방식이었다.

문제
타입과 객체를 통해서 숫자에 문자가 들어오는 것 같은 기본적인 타입 문제들은 해결이 되었다. 그런데 타입은 맞는데 숫자의 범위가 기대하는 것과 다르면 어떻게 될까? 예를 들어서 max-connection 의 값을 0 으로 설정하면 커넥션이 하나도 만들어지지 않는 심각한 문제가 발생한다고 가정해보자.
max-connection 은 최소 1 이상으로 설정하지 않으면 애플리케이션 로딩 시점에 예외를 발생시켜서 빠르게 문제를 인지할 수 있도록 하고 싶다.

외부 설정 사용 - @ConfigurationProperties 검증

예를 들어서 최대 커넥션 숫자는 최소 1 최대 999 라는 범위를 가져야 한다면 어떻게 검증할 수 있을까? 이메일을 외부 설정에 입력했는데, 만약 이메일 형식에 맞지 않는다면 어떻게 검증할 수 있을까?
개발자가 직접 하나하나 검증 코드를 작성해도 되지만, 자바에는 자바 빈 검증기(java bean validation)이라는 훌륭한 표준 검증기가 제공된다.
@ConfigurationProperties 은 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원한다.
자바 빈 검증기를 사용하려면 spring-boot-starter-validation 이 필요하다. build.gradle 에 다음 코드를 추가하자.

implementation 'org.springframework.boot:spring-boot-starter-validation' //추가

검증기를 추가해서 ConfigurationProperties를 만들어보자.

MyDataSourcePropertiesV3

@Getter
@ConfigurationProperties("my.datasource")
@Validated
public class MyDataSourcePropertiesV3 {
 
  	@NotEmpty
 	private String url;
 	@NotEmpty
 	private String username;
 	@NotEmpty
 	private String password;
 	private Etc etc;
 
  	public MyDataSourcePropertiesV3(String url, String username, String password, Etc etc) {
 	this.url = url;
 	this.username = username;
 	this.password = password;
 	this.etc = etc;
 	}

  	@Getter
 	public static class Etc {
 		@Min(1)
 		@Max(999)
 		private int maxConnection;
 		@DurationMin(seconds = 1)
 		@DurationMax(seconds = 60)
 		private Duration timeout;
 		private List<String> options;
 
  		public Etc(int maxConnection, Duration timeout, List<String> options) {
 			this.maxConnection = maxConnection;
 			this.timeout = timeout;
 			this.options = options;
 		}
 	}
}
  • @NotEmpty url , username , password 는 항상 값이 있어야 한다. 필수 값이 된다.

  • @Min(1) @Max(999) maxConnection : 최소 1 , 최대 999 의 값을 허용한다.

  • @DurationMin(seconds = 1) @DurationMax(seconds = 60) : 최소 1, 최대 60초를 허용한다.

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

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

    MyDataSourceConfigV3

    @Slf4j
    @EnableConfigurationProperties(MyDataSourcePropertiesV3.class)
    public class MyDataSourceConfigV3 {
    	private final MyDataSourcePropertiesV3 properties;
    	public MyDataSourceConfigV3(MyDataSourcePropertiesV3 properties) {
    	this.properties = properties;
    	}
    	@Bean
    	public MyDataSource dataSource() {
    		return new MyDataSource(
    			properties.getUrl(),
    			properties.getUsername(),
    			properties.getPassword(),
    			properties.getEtc().getMaxConnection(),
    			properties.getEtc().getTimeout(),
    			properties.getEtc().getOptions());
    	}
    }


    YAML

    스프링은 설정 데이터를 사용할 때 application.properties 뿐만 아니라 application.yml이라는 형식도 지원한다.

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

    application.properties 예시

    environments.dev.url=https://dev.example.com
    environments.dev.name=Developer Setup
    environments.prod.url=https://another.example.com
    environments.prod.name=My Cool App 

    application.yml 예시

    environments:
    	dev:
    		url: "https://dev.example.com"
    		name: "Developer Setup"
    	prod:
    		url: "https://another.example.com"
    		name: "My Cool App" 
    • YAML의 가장 큰 특징은 사람이 읽기 좋게 계층 구조를 이룬다는 점이다.

    • YAML은 space(공백)로 계층 구조를 만든다. space는 1칸을 사용해도 되는데, 보통 2칸을 사용한다.

    • 구분기호로 : 를 사용한다. 만약 값이 있다면 이렇게 key: value: 이후에 공백을 하나 넣고 값을 넣어주면 된다.

      스프링은 YAML의 계층 구조를 properties 처럼 평평하게 만들어서 읽어들인다. 쉽게 이야기해서 위의 application.yml 예시는 application.properties 예시처럼 만들어진다.

      주의
      application.properties, application.yml을 같이 사용하면 application.properties가 우선권을 가진다.
      하지만, 이 둘을 함께 사용하는 것은 일관성이 없으므로 권장하지 않는다.
      실무에서는 설정정보가 많기 때문에 보기 편한 yml을 선호한다.

      YML과 프로필

      YML에도 프로필을 적용할 수 있다.

      application.yml

      my:
      		datasource:
      			url: local.db.com
      			username: local_user
      			password: local_pw
      			etc:
      				maxConnection: 2
      				timeout: 60s
      				options: LOCAL, CACHE

spring:
config:
activate:
on-profile: dev
my:
datasource:
url: dev.db.com
username: dev_user
password: dev_pw
etc:
maxConnection: 10
timeout: 60s

options: DEV, CACHE

spring:
config:
activate:
on-profile: prod
my:
datasource:
url: prod.db.com
username: prod_user
password: prod_pw
etc:
maxConnection: 50
timeout: 10s
options: PROD, CACHE

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


## @Profile
프로필과 외부 설정을 사용해서 각 환경마다 설정값을 다르게 적용하는 것은 이해했다. 
그런데 설정값이 다른 정도가 아니라 각 환경마다 서로 다른 빈을 등록해야 한다면 어떻게 해야할까?
예를 들어서 결제 기능을 붙여야 하는데, 로컬 개발 환경에서는 실제 결제가 발생하면 문제가 되니 가짜 결제 기능이 있는 스프링 빈을 등록하고, 운영 환경에서는 실제 결제 기능을 제공하는 스프링 빈을 등록한다고 가정해보자.

![](https://velog.velcdn.com/images/hyeondooori_/post/965640b1-4ca5-42f3-a16c-2b1c099764b9/image.png)
![](https://velog.velcdn.com/images/hyeondooori_/post/2ec3fcd3-9630-445b-8a0e-74628dee58d9/image.png)

PayConfig
```java
@Slf4j
@Configuration
public class PayConfig {
@Bean
@Profile("default")
public LocalPayClient localPayClient() {
log.info("LocalPayClient 빈 등록");
return new LocalPayClient();
}
@Bean
@Profile("prod")
public ProdPayClient prodPayClient() {
log.info("ProdPayClient 빈 등록");
return new ProdPayClient();
}
}
  • @Profile 애노테이션을 사용하면 해당 프로필이 활성화된 경우에만 빈을 등록한다.
    - default 프로필(기본값)이 활성화 되어 있으면 LocalPayClient 를 빈으로 등록한다.
    - prod 프로필이 활성화 되어 있으면 ProdPayClient 를 빈으로 등록한다.


profile
노는 게 제일 좋은 뽀로로

0개의 댓글