SpringBoot - 외부설정과 프로필(1)

Kwon Yongho·2023년 7월 3일
0

Spring

목록 보기
33/37
post-thumbnail
  1. 외부 설정
  2. 설정 데이터
  3. 우선순위

1. 외부 설정

springboot-external라는 프로젝트를 새로 생성하였습니다.

1-1. 설명

하나의 애플리케이션을 여러 다른 환경에서 사용해야 할 때가 있다.

  • 개발 환경: 개발 서버, 개발 DB 사용
  • 운영 환경: 운영 서버, 운영 DB 사용

문제는 각각의 환경에 따라서 서로 다른 설정값이 존재한다는 점이다. 예를 들어서 애플리케이션이 개발DB
에 접근하려면 dev.db.com이라는 url 정보가 필요한데, 운영DB에 접근하려면 prod.db.com이라는
서로 다른 url을 사용해야 한다.

이렇게 하면 각각의 환경에 맞는 개발app.jar, 운영app.jar가 만들어지므로 해당 파일들을 각 환경별로
배포하면 된다.

단점

  • 환경에 따라서 빌드를 여러번 해야 한다.
  • 개발 버전과 운영 버전의 빌드 결과물이 다르다. 따라서 개발 환경에서 검증이 되더라도 운영 환경에서 다른 빌드 결과를 사용하기 때문에 예상치 못한 문제가 발생할 수 있다. 개발용 빌드가 끝나고 검증한 다음에
    운영용 빌드를 해야 하는데 그 사이에 누군가 다른 코드를 변경할 수도 있다. 한마디로 진짜 같은 소스코드에서 나온 결과물인지 검증하기가 어렵다.
  • 각 환경에 맞추어 최종 빌드가 되어 나온 빌드 결과물은 다른 환경에서 사용할 수 없어서 유연성이 떨어진다. 향후 다른 환경이 필요하면 그곳에 맞도록 또 빌드를 해야 한다.
  • 그래서 보통 다음과 같이 빌드는 한번만 하고 각 환경에 맞추어 실행 시점에 외부 설정값을 주입한다.

실행 시점에 외부 설정값 주입

  • 배포 환경과 무관하게 하나의 빌드 결과물을 만든다. 여기서는 app.jar를 빌드한다. 이 안에는 설정값을 두지 않는다.
  • 설정값은 실행 시점에 각 환경에 따라 외부에서 주입한다.
    • 개발 서버: app.jar를 실행할 때 dev.db.com값을 외부 설정으로 주입한다.
    • 운영 서버: app.jar를 실행할 때 prod.db.com값을 외부 설정으로 주입한다.

유지보수하기 좋은 애플리케이션 개발의 가장 기본 원칙은 변하는 것과 변하지 않는 것을 분리하는 것이다.


외부 설정의 일반적인 방법은 4가지가 있다.

  • OS 환경 변수: OS에서 지원하는 외부 설정, 해당 OS를 사용하는 모든 프로세스에서 사용
  • 자바 시스템 속성: 자바에서 지원하는 외부 설정, 해당 JVM안에서 사용
  • 자바 커맨드 라인 인수: 커맨드 라인에서 전달하는 외부 설정, 실행시 main(args)메서드에서 사용
  • 외부 파일(설정 데이터): 프로그램에서 외부 파일을 직접 읽어서 사용
    • 애플리케이션에서 특정 위치의 파일을 읽도록 해둔다. 예) data/hello.txt
    • 그리고 각 서버마다 해당 파일안에 다른 설정 정보를 남겨둔다.
      • 개발 서버 hello.txt : url=dev.db.com
      • 운영 서버 hello.txt : url=prod.db.com

1-2. OS 환경 변수

OS 환경 변수(OS environment variables)는 해당 OS를 사용하는 모든 프로그램에서 읽을 수 있는 설정값이다.

조회 방법
윈도우 OS: set

애플리케이션에서 OS 환경 변수의 값 읽어보기.

OsEnv

package hello.external;

import lombok.extern.slf4j.Slf4j;

import java.util.Map;

@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));
        }
    }
}
  • System.getenv()를 사용하면 전체 OS 환경 변수를 Map으로 조회할 수 있다.
  • System.getenv(key)를 사용하면 특정 OS 환경 변수의 값을 String으로 조회할 수 있다.

테스트 결과

  • OS 환경 변수를 설정하고, 필요한 곳에서 System.getenv()를 사용해서 외부 설정을 사용 할 수 있다.
  • Ex) 개발 서버, 운영 서버로 설정해놓고 System.getenv("DBURL")로 조회해서 읽는다.
    • 개발 서버: DBURL=dev.db.com
    • 운영 서버: DBURL=prod.db.com

1-3. 자바 시스템 속성

자바 시스템 속성(Java System properties)은 실행한 JVM 안에서 접근 가능한 외부 설정이다. 추가로 자바가 내부에서 미리 설정해두고 사용하는 속성들도 있다.

자바 시스템 속성은 다음과 같이 자바 프로그램을 실행할 때 사용

  • Ex) java -Durl=dev -jar app.jar
  • -D VM 옵션을 통해서 key=value형식을 주면 된다. 이 예제는 url=dev속성이 추가된다.
  • 순서에 주의해야 한다. -D옵션이 - jar보다 앞에 있다.

JavaSystemProperties

package hello.external;

import lombok.extern.slf4j.Slf4j;

import java.util.Properties;

@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)));
        }
    }
}
  • System.getProperties()를 사용하면 Map과 유사한(Map의 자식 타입) key=value형식의
    Properties를 받을 수 있다. (모든 자바 시스템 속성 조회)
  • System.getProperty(key)를 사용하면 속성값을 조회할 수 있다.

테스트 결과

사용자가 직접 정의하는 자바 시스템 속성을 추가(url, username, password를 조회)

        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);

IDE에서 실행시 VM 옵션 추가
-Durl=devdb -Dusername=dev_user -Dpassword=dev_pw

테스트 결과

1-4. 커맨드 라인 인수

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

다음과 같이 사용한다.

  • Ex) java -jar app.jar dataA dataB
  • 필요한 데이터를 마지막 위치에 스페이스로 구분해서 전달하면 된다. 이 경우 dataA, dataB 2개의
    문자가 args에 전달된다.
package hello.external;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CommandLineV1 {
    public static void main(String[] args) {
        for (String arg : args) {
            log.info("arg {}", arg);
        }
    }
}

IDE에서 실행시 커맨드 라인 인수 추가

테스트 결과

key=value 형식 입력
url=devdb username=dev_user password=dev_pw

테스트 결과

  • 이것은 파싱되지 않은, 통 문자이다.
  • 이 경우 개발자가 =을 기준으로 직접 데이터를 파싱해서 key=value형식에 맞도록 분리해야 한다.
    그리고 형식이 배열이기 때문에 루프를 돌면서 원하는 데이터를 찾아야 하는 번거로움도 발생한다.

1-5. 커맨드 라인 옵션 인수

일반적인 커맨드 라인 인수(단순히 띄어쓰기로 구분)

  • aaa bbb -> [aaa, bbb] 값 2개
  • hello world -> [hello, world] 값 2개
  • "hello world" -> [hello world] (공백을 연결하려면 " 를 사용하면 된다.) 값 1개
  • key=value -> [key=value] 값 1개

커맨드 라인 옵션 인수(command line option arguments)
스프링은 커맨드 라인에 -(dash) 2개(--)를 연결해서 시작하면 key=value형식으로 정하고 이것을 커맨드 라인 옵션 인수라 한다.

  • --key=value 형식으로 사용한다.
  • --username=userA --username=userB 하나의 키에 여러 값도 지정할 수 있다.

CommandLineV2

package hello.external;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.DefaultApplicationArguments;

import java.util.List;
import java.util.Set;

@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);
         // mode는 옵션인수 (--)가 아니기 때문에 나오지 않음
        log.info("mode={}", mode);


    }
}

스프링이 제공하는 ApplicationArguments인터페이스와 DefaultApplicationArguments구현체를
사용하면 커맨드 라인 옵션 인수를 규격대로 파싱해서 편리하게 사용할 수 있다.

옵션 인수
--로 시작한다.

  • --url=devdb
  • --username=dev_user
  • --password=dev_pw

테스트 결과

1-6. 커맨드 라인 옵션 인수와 스프링 부트

스프링 부트는 커맨드 라인을 포함해서 커맨드 라인 옵션 인수를 활용할 수 있는 ApplicationArguments를 스프링 빈으로 등록해둔다.

CommandLineBean

package hello;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Set;

@Slf4j
@Component
public class CommandLineBean {
    
    private final ApplicationArguments arguments;
    
    public CommandLineBean(ApplicationArguments arguments) {
        this.arguments = arguments;
    }
    
    @PostConstruct
    public void init() {
        log.info("source {}", List.of(arguments.getSourceArgs()));
        log.info("optionNames {}", arguments.getOptionNames());
        Set<String> optionNames = arguments.getOptionNames();
        for (String optionName : optionNames) {
            log.info("option args {}={}", optionName, arguments.getOptionValues(optionName));
        }
    }
    
}

인수 설정

애플리케이션 실행 결과

1-7. 스프링 통합

  • 외부 설정값이 어디에 위치하든 상관없이 일관성 있고, 편리하게 key=value형식의 외부 설정값을 읽을 수 있으면 사용하는 개발자 입장에서 더 편리하고 또 외부 설정값을 설정하는 방법도 더 유연해질 수 있다.
  • 예를 들어서 외부 설정값을 OS 환경변수를 사용하다가 자바 시스템 속성으로 변경하는 경우에 소스코드를 다시 빌드하지 않고 그대로 사용할 수 있다.
  • 스프링은 이 문제를 EnvironmentPropertySource라는 추상화를 통해서 해결한다.

스프링의 외부 설정 통합

PropertySource

  • 스프링은 PropertySource라는 추상 클래스를 제공하고, 각각의 외부 설정를 조회하는 XxxPropertySource구현체를 만들어두었다.
  • 스프링은 로딩 시점에 필요한 PropertySource들을 생성하고, Environment에서 사용할 수 있게 연결해둔다.

Environment

  • Environment를 통해서 특정 외부 설정에 종속되지 않고, 일관성 있게 key=value형식의 외부 설정에 접근할 수 있다.
    • environment.getProperty(key)를 통해서 값을 조회할 수 있다.
    • Environment는 내부에서 여러 과정을 거쳐서 PropertySource들에 접근한다.
    • 같은 값이 있을 경우를 대비해서 스프링은 미리 우선순위를 정해두었다.
  • 모든 외부 설정은 이제 Environment를 통해서 조회하면 된다.

설정 데이터(파일)
여기에 우리가 잘 아는 application.properties, application.ymlPropertySource에 추가된다. 따라서 Environment를 통해서 접근할 수 있다.

EnvironmentCheck

package hello;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

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

테스트 결과

스프링은 Environment를 통해서 외부 설정을 읽는 방법을 추상화했다. 덕분에 자바 시스템 속성을 사용하다가 만약 커맨드 라인 옵션 인수를 사용하도록 읽는 방법이 변경되어도, 개발 소스 코드는 전혀 변경하지 않아도 된다.

2. 설정 데이터

2-1. 외부 파일

  • 설정 값을 파일에 넣어서 관리하는 방법
  • 애플리케이션 로딩 시점에 해당 파일을 읽어들이면 된다.
  • .properteis라는 파일은 key=vlaue 형식을 사용해서 설정값을 관리하기에 아주 적합

예를 들면 개발 서버와 운영 서버 각각에 application.properties 라는 같은 이름의 파일을 준비해둔다. 그리고 애플리케이션 로딩 시점에 해당 파일을 읽어서 그 속에 있는 값들을 외부 설정값으로 사용하면 된다.

스프링과 설정 데이터

  • 개발자가 파일을 읽어서 설정값으로 사용할 수 있도록 개발을 해야겠지만, 스프링 부트는 이미 이런 부분을 다 구현해두었다. 개발자는 application.properties 라는 이름의 파일을 자바를 실행하는 위치에 만들어 두기만 하면 된다.
  • 스프링이 해당 파일을 읽어서 사용할 수 있는 PropertySource의 구현체를 제공한다. 스프링에서는 이러한 application.properties 파일을 설정 데이터(Config data)라 한다.

동작 확인

  • ./gradlew clean build
  • build/libs로 이동
  • 해당 위치에 application.properties 파일 생성

  • java -jar external-0.0.1-SNAPSHOT.jar실행

이렇게 각각의 환경에 따라 설정 파일의 내용을 다르게 준비하면 된다. 덕분에 설정값의 내용이 많고 복잡해 도 파일로 편리하게 관리할 수 있다.

2-2. 내부 파일 분리

  • 설정 파일을 외부에 관리하는 것은 상당히 번거로운 일이다. 설정을 변경할 때 마다 서버에 들어가서 각각의 변경 사항을 수정해두어야 한다.
  • 이 문제를 해결하는 간단한 방법은 설정 파일을 프로젝트 내부에 포함해서 관리하는 것이다. 그리고 빌드 시점에 함께 빌드되게 하는 것이다.

  • 개발용 설정 파일: application-dev.properties
  • 운영용 설정 파일: application-prod.properties
  1. 빌드 시점에 개발, 운영 설정 파일을 모두 포함해서 빌드한다.
  2. app.jar는 개발, 운영 두 설정 파일을 모두 가지고 배포된다.
  3. 실행할 때 어떤 설정 데이터를 읽어야 할지 최소한의 구분은 필요하다.

스프링은 이미 설정 데이터를 내부에 파일로 분리해두고 외부 설정값(프로필)에 따라 각각 다른 파일을 읽는 방법을 다 구현해두었다.

스프링과 내부 설정 파일 읽기
main/resources에 다음 파일을 추가

application-dev.properties

url=dev.db.com
username=dev_user
password=dev_pw

application-prod.properties

url=dev.db.com
username=dev_user
password=dev_pw

프로필

  • 스프링은 이런 곳에서 사용하기 위해 프로필이라는 개념을 지원한다.
  • spring.profiles.active 외부 설정에 값을 넣으면 해당 프로필을 사용한다고 판단한다.

  • spring.profiles.active=dev
    • dev프로필이 활성화 되었다.
    • application-dev.properties를 설정 데이터로 사용한다.
  • spring.profiles.active=prod
    • prod프로필이 활성화 되었다.
    • application-prod.properties를 설정 데이터로 사용한다.

2-3. 내부 파일 합체

  • 설정 파일을 각각 분리해서 관리하면 한눈에 전체가 들어오지 않는 단점이 있다.
  • 스프링은 이런 단점을 보완하기 위해 물리적인 하나의 파일 안에서 논리적으로 영역을 구분하는 방법을
    제공한다.
  • application.properties라는 하나의 파일 안에서 논리적으로 영역을 나눌 수 있다.
    • application.properties구분 방법 #---또는 !---(dash 3)
    • application.yml구분 방법 ---(dash 3)

설정 데이터를 하나의 파일로 통합하기

앞서 썼던 properties는 주석 처리 했다.

application.properties

spring.config.activate.on-profile=dev
my_url=dev.db.com
my_username=dev_user
my_password=dev_pw
#---
spring.config.activate.on-profile=prod
my_url=prod.db.com
my_username=prod_user
my_password=prod_pw

prod 설정 후 애플리케이션 실행

이제 application.properties라는 파일 하나에 통합해서 다양한 프로필의 설정 데이터를 관리할 수 있다.

3. 우선순위

3-1. 설정 데이터

  • application.properties에서 프로필을 적용하지 않으면 우선순위는 어떻게 될까?
  • 프로필을 적용하지 않고 실행하면 해당하는 프로필이 없으므로 키를 각각 조회하면 값은 null이 된다.


실행 결과를 보면 첫줄에 활성 프로필이 없어서 default라는 이름의 프로필이 활성화 되는 것을 확인할 수 있다. 프로필을 지정하지 않고 실행하면 스프링은 기본으로 default라는 이름의 프로필을 사용한다.

application.properties - 수정

my_url=local.db.com
my_username=local_user
my_password=local_pw

프로필과 무관하게 설정 데이터를 읽어서 사용한다. 이렇게 프로필 지정과 무관하게 사용되는 것을 기본값이라 한다.

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

예시

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

마지막에 url이 있기 때문에 hello.db.com이 설정된다.

3-2. 전체

외부 설정에 대한 우선순위 - 스프링 공식 문서
https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

우선순위는 위에서 아래로 적용된다. 아래가 더 우선순위가 높다.

자주 사용하는 우선순위

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

설정 데이터 우선순위

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

0개의 댓글