본문 바로가기
Spring

[Spring] 외부 설정, @Profile

by 감자b 2024. 12. 28.

외부 설정

하나의 애플리케이션에서 여러 다른 환경을 사용해야 할 때가 있다. (개발용 DB, 운영용 DB…)

즉 환경에 따라 설정값이 달라지게 되는데, 각 환경에 맞게 설정값을 포함하고 jar로 빌드하여 배포하게 되면 빌드를 여러 번 진행해야 하고, 환경마다 빌드의 결과물이 달라 유연성이 떨어진다는 문제가 있다.

따라서 빌드를 한 번만 하고 실행 시점에 각 환경에 맞는 외부 설정 값을 주입한다.

이러한 외부 설정 방법은 4가지 종류가 존재한다.


OS 환경 변수 주입

OS에서 지원하는 외부 설정 방법으로, OS 환경 변수란 해당 OS를 사용하는 모든 프로세스에서 읽을 수 있는 설정 값으로 사용 범위가 가장 넓다.

터미널에서 맥의 경우 printenv 명령어를 사용하여 확인할 수 있다.

애플리케이션에서는 아래와 같은 방법으로 확인 가능하다.

@Slf4j
public class OsEnv {
	public static void main(String[] args) {
		Map<String, String> envMap = System.getenv();
		for (String key : envMap.keySet()) {
			log.info("env {}={}", key, System.getenv(key));
		}
	}
}

자바 시스템 속성

위 방법이 OS 전체에서 사용할 수 있는 환경 변수였다면 자바 시스템 속성은 실행한 JVM 내부에서 접근할 수 있는 외부 설정 방법이다.

해당 속성은 자바 프로그램을 실행할 때 사용한다.

java -Durl=dev -jar app.jar

이렇게 -D{key=value} 형식을 사용하며, -jar 앞에 -D 옵션이 위치해야 한다.

 

만약 Intellij를 사용하면 위 방법 말고도 자바 시스템 속성을 손쉽게 추가할 수 있다.

Edit Configurations… → Modify options → Add VM options → -D{key=value} 추가

 

자바 시스템 속성은 애플리케이션에서 아래와 같이 조회 가능하다.

@Slf4j
public class JavaSystemProperties {
	public static void main(String[] args) {
		Properties properties = System.getProperties();
		for (Object key : properties.keySet()) {
			log.info("prop {}={}", key, System.getProperty(String.valueOf(key)));
		}
	} 
}

자바 커맨드 라인 인수

커맨드 라인 인수는 애플리케이션 실행 시점에 main() 메서드의 args 파라미터로 외부 설정값을 전달하는 방법이다.

java -jar app.jar url=devdb username=dev_user와 같이 마지막 위치에 공백으로 구분하여 설정 값을 전달하면 해당 값들은 main 메서드의 args 배열로 전달된다.

 

마찬가지로 IDE(Intellij)를 사용한다면 쉽게 추가할 수 있다.

Edit Configurations… → Modify options → Program arguments → url=devdb username=dev_user

하지만 args 타입을 보면 String[] 타입이다.

따라서 위에서 봤듯이 key=value 타입이 아닌 문자열이 그대로 전달되므로 개발자가 직접 파싱해서 key,value를 분리해야 한다는 단점이 있다.

 

스프링은 커맨드 라인 인수를 key=value 형식으로 사용할 수 있도록 표준 방식을 정의했는데 이를 커맨드 라인 옵션 인수라고 한다.


커맨드 라인 옵션 인수

위 방법과 유사하지만 --key=value 형식을 입력하면 스프링은 이를 key=value 타입으로 구분해준다.

ex) --username=userA --username=userB

스프링은 ApplicationArguments 인터페이스와 해당 인터페이스의 구현체인 DefaultApplicationArguments를 통해 파싱된 데이터로 편하게 조회할 수 있다.

참고로 ApplicationArguments은 빈으로 등록되어 있으므로 어디서든 주입받아서 사용이 가능하다.

ApplicationArguments appArgs = new DefaultApplicationArguments(args);
log.info("NonOptionArgs = {}", appArgs.getNonOptionArgs()); // 일반 문자열
log.info("OptionNames = {}", appArgs.getOptionNames()); // --key=value

Set<String> optionNames = appArgs.getOptionNames();
for (String optionName : optionNames) {
	log.info("option args {}={}", optionName, appArgs.getOptionValues(optionName));
}

appArgs.getOptionValues(key)의 반환 값은 List인데 이는 하나의 키에 여러 값을 지정할 수 있기 때문이다.

ex) --username=userA --username=userB


스프링 통합 외부 설정

지금까지 살펴본 외부 설정 방법을 보면 모두 key=value 형식이라는 공통점이 있다.

하지만 조회 방법이 모두 달라 불편함이 있는데 스프링은 이를 일관성있게 조회할 수 있도록 Environment 와 PropertySource 라는 추상화를 통해 이 문제를 해결하였다.

스프링은 PropertySource라는 추상 클래스를 제공하고 위에서 설명한 외부 설정을 조회할 수 있는 구현체를 제공한다.

따라서 로딩 시점에 필요한 PropertySeource를 생성하고, Environment 에서 사용할 수 있도록 한다.

environment.getProperty(key)를 통해 어떤 외부 설정이든 공통적으로 값을 조회할 수 있다.

그럼 자바 시스템 속성으로 제공하는 외부 설정과 커맨드 라인으로 넘어오는 외부 설정이 같다면 어떻게 되는걸까?

스프링은 이런 경우 우선 순위를 정해두었다. (아래가 우선 순위가 더 높음)

.properties 설정 파일이 무엇인지는 아래에서 설명한다.

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

설정 데이터 우선순위

  • jar 내부 application.properties
  • jar 내부 프로필 적용 파일 application-{profile}.properties
  • jar 외부 application.properties
  • jar 외부 프로필 적용 파일 application-{profile}.properties

즉 properties 파일에 url=local.db.com이 있고 커맨드 라인 옵션 인수로 --url=dev.db.com가 있다면 우선 순위가 더 높은 --url=dev.db.com가 조회된다.

대부분은 applicaiton.properties에 외부 설정 값을 보관하고 이를 jar 내부에 내장하는 방식을 사용한다.

그리고 변경이 필요하면 더 높은 우선순위를 가지는 방법을 사용해서 유연하게 변경한다.


외부 파일 (설정 데이터)

지금까지 외부 설정 방법은 설정 값이 늘어날수록 보기도 힘들고 관리하기 어렵다는 단점이 있다.

따라서 설정값들을 .properties 또는 yml 파일에 넣고 해당 파일을 로딩 시점에 읽어 사용할 수 있다.

스프링 부트는 application.properties 라는 이름의 파일을 자바를 실행하는 위치에 생성하면 해당 파일을 읽을 수 있는 PropertySource 구현체를 제공, Environment로 조회 가능하다.

(application.yml 형식도 동일하게 적용)

스프링은 각 환경에 맞게 외부 설정을 적용할 수 있도록 프로필이라는 개념을 지원한다.

spring.profiles.active=프로필명

(참고로 프로필은 한 번에 여러 개 지정할 수 있다. --spring.profiles.active=dev,prod)

 

프로필 값 지정 방법

  • 커맨드 라인 옵션 인수 --spring.profiles.active=dev
  • 자바 시스템 속성 -Dspring.profiles.active=dev
  • jar 실행
    • java -Dspring.profiles.active=dev -jar external-0.0.1-SNAPSHOT.jar
    • java -jar external-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
  • 유료 버전의 경우 Edit Configurations… → Active profiles → dev

만약 spring.profiles.active=dev 라면 dev 프로필에 해당하는 외부 설정 값을 사용한다는 의미이다.

spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw

#---은 속성 파일 구분 기호로 아래의 파일은 하나의 파일이지만 논리적으로 각각 다른 파일인 것처럼 구분된다.

yml 파일의 경우 --- 로 구분한다.

이러한 속성 파일 구분 기호 앞, 뒤에는 주석이 있으면 안된다.

 

설정 데이터에는 기본값을 지정할 수 있다.

즉 프로필 지정과 무관하게 해당 값은 항상 사용된다.

url=local.db.com
username=local_user
password=local_pw
#---
spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw

위 properties 파일의 최상단의 경우 어떤 프로필에 설정 값을 사용할 것인지 지정하지 않았다.

이렇게 프로필 정보가 없을 경우 프로필과 무관하게 해당 설정 값은 기본값으로 항상 사용된다.

여기서 url, username, password의 경우 기본값에도 있지만 프로필 설정 값에도 존재한다.

스프링은 문서를 위에서 아래로 읽으면서 사용할 값을 설정한다.

만약 dev 프로필의 경우 맨 위의 기본값들을 등록해놓고 중간에 dev 프로필 설정 값을 확인하고 url, username, password를 해당 설정값으로 대체하며, 마지막은 해당되지 않으므로 무시한다.

따라서 기본값에 dev 프로필 설정값이 덮어씌워지게 된다.

만약 기본값을 맨 아래에 두면 어떤 프로필을 적용하든 기본값이 적용되므로 맨 처음에 두어야 한다.


외부 설정 조회 방법 (@Value, @ConfigurationProperties)

이렇게 설정한 설정 값들을 지금까지는 Environment를 이용해서 조회하였다.

스프링은 Environment를 활용해서 더 편리하게 외부 설정을 읽을 수 있도록 지원한다.

아래의 .properties 파일이 있다고 하자.

my.datasource.url=local.db.com
my.datasource.username=local_user
my.datasource.password=local_pw
my.datasource.etc.max-connection=5
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN

 

@Value

@Value는 내부에 Environment를 사용하여 편하게 외부 설정을 얻어올 수 있도록 지원한다.

아래는 @Value를 필드에서 사용하였는데, 메서드의 파라미터에서도 사용할 수 있다.

@Value("${my.datasource.etc.max-connection:1}")의 경우 뒤에 :1이 붙어있는 것을 확인할 수 있는데 이는 해당 key의 값이 없는 경우 기본값으로 1을 사용한다는 의미이다.

@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:1}")
private int maxConnection;

@Value("${my.datasource.etc.timeout}")
private Duration timeout;

@Value("${my.datasource.etc.options}")
private List<String> options;

참고로 @Value는 필드 주입 시 생성자가 호출된 직후, 의존관계 주입 시점에 값이 채워진다. 즉, 생성자가 호출될 때는 null이므로 유의

 

@ConfigurationProperties

.properties 파일을 보면 my.datasource라는 공통적인 계층을 가지고 외부 값을 설정한다.

설정 파일의 경우 보통 이렇게 객체의 묶음으로 관리를 하는데, 위 방법은 이렇게 공통 부분이 있음에도 하나하나 설정 데이터를 받아온다는 단점이 있다.

@ConfigurationProperties는 외부 설정의 묶음 정보를 객체로 변환한다.

따라서 설정 정보가 객체를 사용하여 타입을 가지기 때문에 잘못된 타입이 들어오는 문제를 방지할 수 있어, 타입 안전한 설정 속성이라고 한다.

 

위 .properties 파일의 설정 값을 아래와 같이 객체로 정의해서 얻어올 수 있다.

@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 를 통해 해당 객체는 외부 설정을 주입 받는 객체임을 명시한다.

즉 키가 my.datasource로 시작하는 값을 얻어온다.

기본적으로 자바빈 프로퍼티 방식으로 주입받으므로 getter, setter가 필요하다.

 

그리고 @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)를 통해 스프링에게 사용할 @ConfigurationProperties를 지정한다.

따라서 해당 클래스는 스프링 빈으로 등록되어 필요한 곳에서 주입 받아서 사용이 가능하다.

@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());
	} 
}

이렇게 하면 maxConnection=abc와 같이 int 값에 문자가 들어올 경우 오류가 발생하므로 타입 안전하게 객체 사용이 가능하다.

추가로 스프링은 캐밥 표기법을 자바 카멜 케이스 표기법으로 중간에서 자동으로 변환

  • 캐밥 표기법
    • .properties 파일은 max-connection과 같이 단어를 -로 구분하여 사용한다.
    • 자바에선 주로 카멜 케이스 표기하는데, 스프링이 위 캐밥 표기를 카멜 케이스 형식으로 바꿔준다.

설정 정보의 경우 초기화가 이루어지면 불변으로 설정하는 것이 일반적이다.

하지만 위 방법은 setter가 존재하여 설정 값이 변경될 수 있다.

따라서 setter가 아닌 생성자를 통하여 설정 객체를 초기화할 수 있다.

@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {
	private String url;
	private String username;
	private String password;
	private Etc etc;
    
	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;
		}
	}
}

이렇게 생성자를 만들어두면 해당 생성자를 통해 설정 정보를 주입받는다.

스프링 부트 3.0 이전에는 생성자에 @ConstructorBinding을 사용해야 했지만 지금은 생략해도 된다.

 

@DefaultValue는 해당 값을 찾을 수 없을 때 기본값을 지정한다.

  • @DefaultValue Etc etc : etc가 없다면 Etc 객체를 생성하고 내부에 들어가는 값은 비움. (null 또는 0)
  • @DefaultValue("DEFAULT") List<String> options : options이 없다면 DEFAULT라는 값을 사용한다는 의미.

@ConfigurationProperties 검증

@ConfigurationProperties 를 이용해서 외부 설정을 주입받는 객체는 자바 빈 검증기(java bean validation)를 통해 검증할 수 있다.

아래와 같이 검증이 가능하다.

@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;
		}
	}
}

@Profile

만약 각 환경마다 서로 다른 빈을 등록해야 한다면?

@Profile 애노테이션을 사용하면 해당 프로필이 활성화 되었을 때만 해당 빈을 등록한다.

@Slf4j
@Configuration
public class PayConfig {
	@Bean
	@Profile("default")
	public LocalPayClient localPayClient() {
		return new LocalPayClient();
	}
    
	@Bean
	@Profile("prod")
	public ProdPayClient prodPayClient() {
		return new ProdPayClient();
	}
}

 

@Profile 내부 구조는 다음과 같다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
    String[] value();
}

@Profile 은 특정 조건에 따라서 해당 빈을 등록할지 말지 선택한다.

이렇게 필터링할 수 있는 이유는 @Conditional({ProfileCondition.class})를 사용하기 때문이다.

즉 @Profile 을 사용하면 환경마다 외부 설정 값을 분리하고, 등록되는 스프링 빈 또한 분리할 수 있다.


참고

 

스프링 부트 - 핵심 원리와 활용 강의 | 김영한 - 인프런

김영한 | 실무에 필요한 스프링 부트는 이 강의 하나로 모두 정리해드립니다., 백엔드 개발자를 위한 스프링 부트 끝판왕! 실무에 필요한 내용을 모두 담았습니다.  [임베딩 영상] 김영한의 스

www.inflearn.com

 

'Spring' 카테고리의 다른 글

[Spring] API 및 예외 응답 통일  (0) 2025.01.11
[Spring] OAuth2.0을 이용한 네이버, 구글 소셜 로그인 구현  (0) 2025.01.02
[Spring] Auto Configuration  (0) 2024.12.28
[Spring-boot] JAR, WAR  (0) 2024.12.28
[Spring AOP] @Aspect AOP  (0) 2024.12.28