하나의 애플리케이션을 여러 다른 환경에서 사용해야할 때가 있다. 대표적으로 개발이 잘 진행되고 있는지 확인하는 용도의 개발 환경, 그리고 실제 고객에게 서비스하는 운영 환경이 있다.
개발 환경: 개발 서버, 개발 DB 사용
운영 환경: 운영 서버, 운영 DB 사용
문제는 각각의 환경에 따라 서로 다른 설정값이 존재한다는 점이다. 예를 들어 애플리케이션이 개발 DB에 접근하려면 dev.db.com이라는 url 정보가 필요한데, 운영 DB에 접근하려면 prod.db.com이라는 서로 다른 url을 사용해야한다.
이 문제를 해결하는 가장 단순한 방법은 다음과 같이 각각의 환경에 맞게 애플리케이션을 빌드하는 것이다.

개발 환경에는 dev.db.com이 필요하므로 이 값을 애플리케이션 코드에 넣은 다음에 빌드해서 개발app.jar를 만든다.
운영 환경에는 prod.db.com이 필요하므로 이 값을 애플리케이션 코드에 넣은 다음 빌드해서 운영app.jar을 만든다.
하지만 이것은
그래서!
보통 빌드는 한 번만 하고, 각 환경에 맞추어 실행 시점에 외부 설정값을 주입한다.
환경에 따라 변하는 설정값을 실행 시점에 주입

app.jar을 빌드한다. 이 안에는 설정값을 두지 않는다.app.jar를 실행할 때 dev.db.com값을 외부 설정으로 주입한다.app.jar를 실행할 때 prod.db.com값을 외부 설정으로 주입한다.이렇게 하면 빌드도 한번만 하면 되고, 개발 버전과 운영 버전의 빌드 결과물이 같기 때문에 개발 환경에서 검증되면 운영 환경에서도 믿고 사용할 수 있다. 그리고 이후에 새로운 환경이 추가되어도 별도의 빌드 과정 없이 기존 app.jar를 사용하여 손쉽게 새로운 환경을 추가할 수 있다.
📌유지보수하기 좋은 애플리케이션 개발의 가장 기본 원칙 - 변하는 것과 변하지 않는 것을 분리
애플리케이션을 실행할 때 필요한 설정값을 외부에서 어떻게 불러와서 애플리케이션에 전달할 수 있을까?

main(args)메서드에서 사용📌 스프링은 OS환경변수, 자바 시스템 속성, 자바 커멘드 라인 인수의 3가지 방식을 모두 통합하여 편리하게 제공해준다. 우리는 그 편리함을 알기 위해 이 것을 공부한다!
OS 환경변수(OS environment variables)는 해당 OS를 사용하는 모든 프로그램에서 읽을 수 있는 설정값이다. 한마디로 다른 외부설정과 비교하여 사용 범위가 가장 넓다.
윈도우 OS: set, MAC,리눅스: printenv로 조회할 수 있다.

이렇게 현재 OS에 설정된 OS 환경 변수 값들을 출력했다.
@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 환경변수를 설정하고, 필요한 곳에서 System.getenv()를 사용하면 외부 설정을 사용할 수 있다.
이제 데이터베이스 접근 URL과 같은 정보를 OS 환경변수에 설정해두고 읽어들이면 된다.
예를 들어 개발 서버에서는 DBURL=dev.db.com과 같이 설정하고, 운영 서버에서는 DBURL=prod.db.com과 같이 설정하는 것이다.
이렇게하면 System.getenv("DBURL") 을 조회할 때 각각 환경에 따라서 서로 다른 값을 읽게 된다.
하지만 OS 환경 변수는 이 프로그램 뿐만 아니라 다른 프로그램에서도 사용할 수 있다. 쉽게 이야기해서 전역 변수 같은 효과가 있다. 여러 프로그램에서 사용하는 것이 맞을 때도 있지만, 해당 애플리케이션을 사용하는 자바 프로그램 안에서만 사용되는 외부 설정값을 사용하고 싶을 때도 있다.
자바 시스템 속성(Java System properties)는 실행할 JVM 안에서 접근 가능한 외부 설정이다. 추가로 자바가 내부에서 미리 설정해두고 사용하는 속성들도 있다.
자바 시스템 속성은 다음과 같이 자바 프로그램을 실행할 때 사용한다.
-D VM 옵션을 통해서 key=value 형식을 주면 된다. 이 예제는 url=dev 속성이 추가된다.-D 옵션이 - jar 보다 앞에 있다.@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)));
}
}
}

자바가 제공하는 수 많은 속성들이 추가되어 있는 것을 확인할 수 있다. 자바는 내부에서 필요할 때 이런 속성들을 사용하는데, 예를 들어 file.encoding=UTF-8을 통해 기본적인 파일 인코딩 정보 등으로 사용한다.
이번에는 사용자가 직접 정의하는 자바 시스템 속성을 추가해보자.
url, username, password를 조회하는 코드를 추가하자.
@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)));
}
String url = System.getProperty("url");
String username = System.getProperty("username");
String password = System.getProperty("password");
log.info("url={}", url);
log.info("username={}", username);
log.info("password={}", password);
}
}


커맨드 라인 인수(command line arguments)는 애플리케이션 실행 시점에 외부 설정값을 main(args) 메서드의 args 파라미터로 전달하는 방법이다.
@Slf4j
public class CommandLineV1 {
public static void main(String[] args) {
for (String arg : args) {
log.info("arg {}", arg);
}
}
}


일반적 커맨드 라인 인수
커맨드 라인에 전달하는 값은 형식이 없고 단순히 띄어쓰기로 구분한다.
커맨드 라인 옵션 인수(command line option arguments)
커맨드 라인 인수를 key=value 형식으로 구분하는 방법이 필요하다. 그래서 스프링에서는 커맨드 라인 인수를 key=value 형식으로 편리하게 사용할 수 있도록 스프링만의 표준 방식을 정의했는데, 그것이 바로 커멘드 라인 옵션 인수이다.
스프링은 커맨드 라인에 - (dash) 2개(--)를 연결해서 시작하면 key=value 형식으로 정하고 이것을 커맨드 라인 옵션 인수라 한다.
@Slf4j
public class CommandLineV2 {
public static void main(String[] args) {
for (String arg : args) {
log.info("arg {}", arg);
}
ApplicationArguments appArgs = new DefaultApplicationArguments(args);
log.info("SourceArgs = {}", List.of(appArgs.getSourceArgs()));
log.info("NonOptionArgs = {}", appArgs.getNonOptionArgs());
log.info("OptionNames = {}", appArgs.getOptionNames());
Set<String> optionNames = appArgs.getOptionNames();
for (String optionName : optionNames) {
log.info("option args {}={}", optionName,
appArgs.getOptionValues(optionName));
}
List<String> url = appArgs.getOptionValues("url");
List<String> username = appArgs.getOptionValues("username");
List<String> password = appArgs.getOptionValues("password");
List<String> mode = appArgs.getOptionValues("mode");
log.info("url={}", url);
log.info("username={}", username);
log.info("password={}", password);
log.info("mode={}", mode);
}
}
스프링이 제공하는 ApplicationArguments 인터페이스와 DefaultApplicationArguments 구현체를 사용
하면 커맨드 라인 옵션 인수를 규격대로 파싱해서 편리하게 사용할 수 있다.



커맨드 라인 옵션 인수, 자바 시스템 속성, OS 환경변수는 모두 외부 설정을 key=value 형식으로 사용할 수 있는 방법이다. 그런데 이 외부 설정값을 읽어서 사용하는 개발자 입장에서 단순하게 생각해보면, 모두 key=value 형식이고, 설정값을 외부로 뽑아둔 것이다. 그런데 어디에 있는 외부 설정값을 읽어야하는지에 따라 각자 읽어야 하는 방법이 다르다는 단점이 있다.
예를 들어서 OS 환경변수에 두면 System.getenv(key)를 사용해야하고, 자바 시스템 속성을 사용하면 System.getProperty(key)를 사용해야한다.
만약 OS에 환경 변수를 두었는데, 이후에 정책이 변경되어서 자바 시스템 속성에 환경 변수를 두기로 했다고 가정해보자. 그러면 해당 코드들을 모두 변경해야한다.
외부 설정값이 어디에 위치하든 상관없이 일관성 있고, 편리하게 key=value형식의 외부 설정값을 읽을 수 있으면 사용하는 개발자 입장에서 더 편리하고 또 외부 설정값을 설정하는 방법도 더 유연해질 수 있다. 예를 들어서 외부 설정값을 OS 환경변수를 사용하다가 자바 시스템 속성으로 변경하는 경우에 소스코드를 다시 빌드하지 않고 그대로 사용할 수 있다.
스프링은 이 문제를 Environment와 PropertySource라는 추상화를 통해 해결한다.

org.springframework.core.env.PropertySource
스프링은 PropertySource라는 추상클래스를 제공하고, 각각의 외부 설정을 조회하는 XxxPropertySource 구현체를 만들어두었다.
CommandLinePropertySource
SystemEnvironmentPropertySource
스프링은 로딩 시점에 필요한 PropertySource들을 생성하고, Environment에서 사용할 수 있게 연결해둔다.
org.springframework.core.env.Environment
Environment를 통해서 특정 외부 설정에 종속되지 않고, 일관성 있게 key=value 형식의 외부 설정에 접근할 수 있다.
environment.getProperty(key)💻모든 외부 설정은 Environment를 통해 조회하면 된다~!
설정 데이터(파일)
여기에 우리가 잘 아는 application.properties , application.yml 도 PropertySource 에 추가된다.
따라서 Environment 를 통해서 접근할 수 있다.
@Slf4j
@Component
public class EnvironmentCheck {
private final Environment env;
public EnvironmentCheck(Environment env) {
this.env = env;
}
@PostConstruct
public void init() {
String url = env.getProperty("url");
String username = env.getProperty("username");
String password = env.getProperty("password");
log.info("env url={}", url);
log.info("env username={}", username);
log.info("env password={}", password);
}
}
커맨드 라인 옵션 인수 실행
--url=devdb --username=dev_user --password=dev_pw
자바 시스템 속성 실행
-Durl=devdb -Dusername=dev_user -Dpassword=dev_pw

커맨드 라인 옵션 인수, 자바 시스템 속성 모두 Environment 를 통해서 동일한 방법으로 읽을 수 있는 것을 확인했다.
스프링은 Environment 를 통해서 외부 설정을 읽는 방법을 추상화했다. 덕분에 자바 시스템 속성을 사용하다가 만약 커맨드 라인 옵션 인수를 사용하도록 읽는 방법이 변경되어도, 개발 소스 코드는 전혀 변경하지 않아도 된다.
예를 들어 커맨드 라인 옵션 인수와 자바 시스템 속성을 다음과 같이 중복해서 설정하면 어떻게 될까?
커맨드 라인 옵션 인수 실행
--url=proddb --username=prod_user --password=prod_pw
자바 시스템 속성 실행
-Durl=devdb -Dusername=dev_user -Dpassword=dev_pw
우선순위) 커맨드 라인 옵션 인수 > 자바 시스템 속성
지금까지 학습한 OS 환경 변수, 자바 시스템 속성, 커맨드 라인 옵션 인수는 사용해야 하는 값이 늘어날수록 사용하기가 불편해진다. 실무에서는 수십개의 설정값을 사용하기도 하므로 이런 값들을 프로그램을 실행할 때마다 입력하는 것은 번거롭고 관리도 어렵다.
그래서 등장하는 대안은 설정값을 파일에 넣어 관리하는 것이다. 그리고 애플리케이션 로딩 시점에 해당 파일을 읽어들이면 된다. 그 중에서도 .properties라는 파일은 key=value형식을 사용해서 설정값을 관리하기에 아주 적합하다.

개발자가 파일을 읽어 설정값으로 사용할 수 있도록 개발을 해야겠지만, 스프링 부트는 이미 이런 부분을 다 구현해두었다. 개발자는 application.properties라는 이름의 파일을 자바를 실행하는 위치에 만들어두기만 하면 된다.
그러면 스프링이 해당 파일을 읽어서 사용할 수 있는 PropertySource의 구현체를 제공한다.
스프링에서는 이러한 application.properties파일을 설정 데이터(Config data)라고 한다.
당연히 설정 데이터도 Environment를 통해 조회할 수 있다.
📌 여전히 남은 문제
외부 설정을 별도의 파일로 관리하게 되면 설정 파일 자페를 관리하기 번거로운 문제가 발생한다. 서버가 10대면 변경사항이 있을 때 10대 서버의 설정 파일을 모두 각각 변경해야하는 불편함이 있다.
설정 파일이 별도로 관리되기 때문에 설정값의 변경 이력을 확인하기 어렵다.
특히 설정값의 변경 이력이 프로젝트 코드들과 어떻게 영향을 주고받는지 그 이력을 확인하기 어렵다.
설정 파일을 외부에 관리하는 것은 상당히 번거로운 일이다. 설정을 변경할 때마다 서버에 들어가서 각각의 변경 사항을 수정해두어야한다. (물론 이것을 자동화 하기 위해 노력할 수 있다.)
이 문제를 해결하는 간단한 방법은 설정 파일을 프로젝트 내부에 포함해서 관리하고, 빌드 시점에 함께 빌드되게 하는 것이다.
이렇게하면 애플리케이션을 배포할 때 설정 파일의 변경사항도 함께 배포할 수 있다. 쉽게 얘기해서 jar 하나로 설정 데이터까지 포함해서 관리하는 것이다.
프로젝트 안에 소스 코드 뿐만 아니라 각 환경에 필요한 설정 데이터도 함께 포함해서 관리한다.
dev 라는 값을 제공하고, 운영 서버는 prod 라는 값을 제공하자. 편의상 이 값을 프로필이라 하자.스프링은 이미 설정 데이터를 내부에 파일로 분리해두고 외부 설정값(프로필)에 따라 각각 다른 파일을 읽는 방법을 다 구현해두었다.

--spring.profiles.active=dev-Dspring.profiles.active=dev./gradlew clean buildbuild/libs 로 이동java -Dspring.profiles.active=dev -jar external-0.0.1-SNAPSHOT.jarjava -jar external-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
📌 남은 문제
설정 파일을 각각 분리해서 관리하면 한눈에 전체가 들어오지 않는 단점이 있다.
설정 파일을 각각 분리해서 관리하면 한눈에 전체가 들어오지 않는 단점이 있다.
스프링은 이런 단점을 보완하기 위해 물리적인 하나의 파일 안에서 논리적으로 영역을 구분하는 방법을 제공한다.

기존에는 dev 환경은 application-dev.properties , prod 환경은 application-prod.properties 파일이 필요했다.
스프링은 하나의 application.properties 파일 안에서 논리적으로 영역을 구분하는 방법을 제공한다.
application.properties 라는 하나의 파일 안에서 논리적으로 영역을 나눌 수 있다.
#--- 또는 !--- (dash 3)--- (dash 3)그림의 오른쪽 application.properties 는 하나의 파일이지만 내부에 2개의 논리 문서로 구분되어 있다.
프로필에 따라 논리적으로 구분된 설정 데이터를 활성화 하는 방법
spring.config.activate.on-profile 에 프로필 값 지정





기본값
내 pc에서 개발하는 것을 보통 로컬(local) 개발 환경이라 한다. 이때도 항상 프로필을 지정하면서 실행하는 것은 상당히 피곤할 것이다.
설정 데이터에는 기본값을 지정할 수 있는데, 프로필 지정과 무관하게 이 값은 항상 사용된다.

스프링은 문서를 위에서 아래로 순서대로 읽으며 설정한다.
위 코드에는 spring.config.activate.on-profile 와 같은 프로필 정보가 없다.
따라서 프로필과 무관하게 설정 데이터를 읽어서 사용한다.
이렇게 프로필 지정과 무관하게 사용되는 것을 기본값이라고 한다.
특정 프로필을 지정하지 않고 실행하면?

기본값이 사용된 것을 확인할 수 있다.
프로필 설정값을 주면, 당연히 기본값보다는 우선권을 가지므로, 준 값이 젓용된다.
application.properties
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
스프링은 단순하게 문서를 위에서 아래로 순서대로 읽으며 사용할 값을 설정한다.
1. 순서상 위에 있는 local 관련 논리 문서의 데이터들을 읽어서 설정한다. 여기에는 spring.config.activate.on-profile 와 같은 별도의 프로필을 지정하지 않았기 때문에 프로필과 무관하게 항상 값을 사용하도록 설정한다.
url=local.db.com
username=local_user
password=local_pw
url=dev.db.com -> prod.db.com
username=dev_user -> prod_user
password=dev_pw -> prod_pw
📌 참고로, 프로필을 한 번에 둘 이상 설정하는 것도 가능하다.
--spring.profiles.active=dev,prod
조금 더 극단적 예시를 통해 순서를 확실히 이해해보자!
application.properties - 수정
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
#---
url=hello.db.com
스프링이 설정 파일을 위에서 아래로 순서대로 읽어 사용할 값을 설정한다는 것!
위에서 아래로 순서대로 실행하는데, 마지막에는 프로필이 없기 때문에 항상 마지막의 값들을 적용하게 된다.
만약 prod 프로필을 사용한다면 다음과 같이 설정된다.
The following 1 profile is active: "prod"
...
url=hello.db.com
username=prod_user
password=prod_pw
물론 이렇게 사용하는 것은 의미가 없다. 이해를 돕기 위해 극단적 예시를 사용했다!
정리하면 다음과 같다.

스프링 부트는 같은 애플리케이션 코드를 유지하면서 다양한 외부 설정을 사용할 수 있도록 지원한다.
우선순위) 커맨드라인 옵션 인수 > 자바 시스템 속성 > OS 환경변수
Environment 를 통해서 조회하는 관점에서 보면 외부 설정값들은 계속 추가되거나 기존 값을 덮어서 변경하는 것 처럼 보인다. 물론 실제 값을 덮어서 변경하는 것은 아니고, 우선순위가 높은 값이 조회되는 것이다. 그런데 이렇게 이해하면 개념적으로 더 쉽게 이해할 수 있다.

정리
이렇게 우선순위에 따라서 설정을 추가하거나 변경하는 방식은 상당히 편리하면서도 유연한 구조를 만들어준다. 실무에서 대부분의 개발자들은 applicaiton.properties 에 외부 설정값들을 보관한다. 이렇게 설정 데이터를 기본으로 사용하다가 일부 속성을 변경할 필요가 있다면 더 높은 우선순위를 가지는 자바 시스템 속성이나 커맨드 라인 옵션 인수
를 사용하면 되는 것이다.
또는 기본적으로 application.properties 를 jar 내부에 내장하고 있다가, 특별한 환경에서는 application.properties 를 외부 파일로 새로 만들고 변경하고 싶은 일부 속성만 입력해서 변경하는 것도 가능하다.