Spring Boot

Spring Boot의 외부설정

작은별._. 2023. 12. 25. 22:06
728x90

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

각각의 환경에 따라 서로 다른 설정값이 존재하는데, 설정값에 따른 애플리케이션을 따로 만들어 빌드를 여러 번 하여 배포해도 되지만, 이렇게 할 경우 유연성이 떨어지고, 다른 환경이 추가되면 또 거기에 맞도록 코드를 수정한 뒤 빌드를 해야 하는 번거로움이 존재합니다. 그래서 보통 아래와 같이 빌드는 한 번만 하고, 각 환경에 맞추어 실행 시점에 외부 설정값을 주입합니다.  


 

 

이렇게 변하는 것 (외부 설정값)과 변하지 않는 것 (코드와 빌드 결과물)을 분리하여 유지보수하기 좋은 애플리케이션을 개발할 수 있고, 빌드 과정을 줄이며 환경에 따른 유연성을 확보할 수 있습니다.

 

이번 포스팅은 이렇게 애플리케이션을 실행할 때 필요한 외부 설정값을 어떻게 불러와 애플리케이션에 전달할 수 있는지에 대해서 작성하였습니다.


외부 설정은 일반적으로 아래와 같은 4가지 방법이 있습니다.

 

  • OS 환경 변수
    • OS에서 지원하는 외부 설정으로, 해당 OS를 사용하는 모든 프로세스에서 사용 (like 전역변수)
  • 자바 시스템 속성
    • 자바에서 지원하는 외부 설정으로, 해당 JVM 안에서 사용
  • 자바 커맨드 라인 인수
    • 커맨드 라인에서 전달하는 외부 설정으로, 실행 시 main(args) 메서드에서 사용
  • 외부 파일 (설정 데이터)
    • 프로그램에서 외부 파일을 직접 읽어서 사용하는 방식
    • Spring boot에서 많이 사용하는 방식입니다.

 

이번 포스팅은 외부 파일을 제외한 3가지 방법과, 스프링 부트를 이용하여 편리하게 외부 설정을 읽어 들이는 방법에 대해서 작성하였습니다.


외부 설정 - OS 환경 변수

애플리케이션에서 OS 환경 변수 값을 읽는 방법은 아래와 같습니다.


import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Map;

public class OsEnvApp{

    public static void main(String[] args) {
        Map<String, String> envMap = System.getenv();
        for(String key: envMap.keySet()){
            System.out.printf("env %s = %s", key, envMap.get(key)); // System.getenv(key);와 동일
        }
    }
}
  • System.getenv() : 전체 OS 환경 변수를 Map으로 조회
  • System.getenv(key) : 특정 OS 환경 변수의 값을 String으로 조회

[실행 결과]

OS 환경변수 조회

 

OS 환경 변수는 같은 OS를 공유하는 다른 프로그램에서도 사용할 수 있기 때문에 여러 프로그램에서 사용하는 것이 유용할 수도 있지만 각 애플리케이션마다 다른 값을 할당하고 싶을 때는 제약이 존재합니다. 


외부 설정 - 자바 시스템 속성

자바 시스템 속성은 실행한 JVM 안에서 접근 가능한 외부 설정입니다. 아래와 같이 자바 프로그램을 실행할 때 사용합니다.

  • java -Durl=dev -jar app.jar
  • -D VM 옵션을 통해 key=value 형식으로 입력합니다. 이 때, -D 옵션이  -jar 보다 앞에 있음을 주의하여야 합니다.
    • 혹은, IDE에서 실행 시 VM 옵션을 추가할 수 있습니다.

public class JavaSystemProperties {
    public static void main(String[] args) {
        Properties properties = System.getProperties();
        for(Object key: properties.keySet()){
            System.out.printf("prop  %s = %s\n", key, System.getProperty(String.valueOf(key)));
        }
    }
}
  • System.getProperties(): Map과 유사한 key=value 형식의 Properties를 통해 모든 자바 시스템 속성 조회
  • System.getProperty(key): 속성값 조회 

아래와 같이 자바가 기본으로 제공하는 수많은 속성들을 확인할 수 있습니다. 자바는 내부에서 이런 속성들을 사용하여 프로그램을 실행시킵니다. 

 

자바 시스템 속성 조회


 

여기에 VM 옵션을 추가하면 아래와 같은 결과를 얻을 수 있습니다.

VM 옵션 추가



코드 변경


public class JavaSystemProperties {
    public static void main(String[] args) {
        System.out.printf("url = %s\n", System.getProperty("url"));
        System.out.printf("username = %s\n", System.getProperty("username"));
        System.out.printf("pwd = %s\n", System.getProperty("password"));
    }

}

[실행 결과]


외부 설정 - 커맨드 라인 인수

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

아래와 같이 사용합니다.

  • java -jar app.jar dataA dataB
  • 필요한 데이터를 마지막 위치에 space로 구분해서 전달하면 됩니다. 이 경우는 dataA, dataB가 args에 전달됩니다.

public class CommanLine {

/*
 커맨드 라인에 전달하는 값은 형식이 없고, 단순히 띄어쓰기로 구분
 - aaa bbb -> [aaa, bbb] 값 2개
 - "hello world" -> [hello world] 값 1개 (공백 연결시 " " 사용)
 - key=value -> [key=value] 값 1개 (이 경우, 개발자가 직접 파싱 필요)
*/
    public static void main(String[] args) {
        for(String arg: args){
            System.out.printf("arg %s\n", arg);
        }
    }

}

 

 

위 주석처리 된 내용을 보면, key=value로 입력 시, key=value 값이 그대로 출력되어서 나오게 될 것을 예측할 수 있습니다. 실제로 인자로 key=value 형식으로 값을 넣어보았더니 아래와 같은 결과를 얻었습니다.



[실행 결과]

 

 

커맨드 라인 옵션 인수 (command line option arguments)

개발자가 직접 파싱해야 하는 불편함을 줄이기 위하여 스프링에서 커맨드 라인 옵션 인수라는 표준 방식을 정의하여 제공하고 있습니다. 커맨드 라인에 '-'를 2개 연결  (--)  해서 시작하면 key=value 형식으로 정하고, 이것을 커맨드 라인 옵션 인수라고 합니다.

  • --key-value 형식
  • --key=val1 --key=val2처럼 하나의 키에 여러 값도 지정 가능합니다.
  • 해당 값들을 읽어 들이기 위해서 스프링에서는 ApplicationArguments 인터페이스를 제공합니다. DefaultApplicationArguments 구현체를 통해 커맨드 라인 옵션 인수를 규격대로 파싱 해서 사용할 수 있습니다.

public class CommandLine{

    public static void main(String[] args) {
        for(String arg: args){
            System.out.printf("arg %s\n", arg); // 커맨드 라인 입력 결과 그대로 출력
        }

        ApplicationArguments appArgs = new DefaultApplicationArguments(args);

        // SourceArgs: 커맨드 라인 인수 전부 출력
        System.out.printf("SourceArgs = %s\n", List.of(appArgs.getSourceArgs()));

        // NonOptionArgs: 옵션 인수가 X, key=value 형식으로 파싱되지 않은 인수들 (--가 없는 인수들)
        // mode=on
        System.out.printf("NonOptionArgs = %s\n", appArgs.getNonOptionArgs());

        // OptionName: key=value 형식으로 사용되는 옵션 인수들 (--가 있는 인수들)
        // url, username, password
        System.out.printf("OptionNames = %s\n", appArgs.getOptionNames());

        List<String> url = appArgs.getOptionValues("url");
        List<String> username = appArgs.getOptionValues("username");
        List<String> password = appArgs.getOptionValues("password");
        List<String> mode = appArgs.getOptionValues("mode");

        System.out.printf("url = %s\n", url);
        System.out.printf("username = %s\n", username);
        System.out.printf("password = %s\n", password);
        System.out.printf("mode = %s\n", mode); // null 값

    }


}

 

  • 옵션 인수는 --username=user1 --username=user2처럼 하나의 key에 여러 value을 할당할 수 있기 때문에 appArgs.getOptionValues(key)의 결과는 List로 반환합니다.

커맨드 라인 옵션 인수는 자바 언어의 표준 기능이 아니라, 스프링이 편리함을 위해 제공하는 기능입니다.

아래 코드는 스프링 부트를 이용하여 커맨드 라인 옵션 인수를 사용하였습니다. 스프링 부트는 ApplicationArguments를 스프링 빈으로 등록해 주기 때문에 별도로 객체를 생성할 필요가 없습니다.


Spring Boot Version


  

@Component
public class CommandLineBean {
    private final ApplicationArguments arguments;
    public CommandLineBean(ApplicationArguments arguments) {
        this.arguments = arguments;
    }
    @PostConstruct
    public void init() {
        System.out.printf("source %s\n", List.of(arguments.getSourceArgs()));
        System.out.printf("optionNames %s\n", arguments.getOptionNames());
        Set<String> optionNames = arguments.getOptionNames();
        for (String optionName : optionNames) {
            System.out.printf("option args %s=%s\n", optionName,
                    arguments.getOptionValues(optionName));
        }
    }
}

 

 

그 후, application.main 함수를 실행하면 아래와 같은 결과를 얻을 수 있습니다.


// main 함수
@SpringBootApplication
public class SpringbootApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.class, args);
    }

}

 

 

 

[실행 결과]

 


이렇게 3가지 방법으로 외부 설정을 읽어 보았습니다. 하지만 여기에는 문제가 있습니다. 외부 설정값을 읽는 방식에 따라 읽는 방법이 다르다는 것입니다. 즉, OS 환경 변수에 있는 값을 읽으려면 System.getEnv(key)를, 자바 시스템 속성을 통해 외부 설정 값을 읽어야 한다면 System.getProperty(key) 함수를 사용해야 합니다. 

따라서, 외부 설정값이 어디에 위치하든 상관없이 일관성 있고 편리하게 외부 설정값을 읽는 것이 코드 변경 및 유지 보수에 용이할 것입니다. 예를 들어서 외부 설정 값을 OS 환경변수를 사용하다가 자바 시스템 속성으로 변경하는 경우에 소스코드를 다시 빌드하지 않고 그대로 사용하는 것이 편리하겠죠.

 

스프링은 Environment와 PropertySource라는 추상화를 통해 이를 해결합니다.


스프링의 외부 설정 통합


PropertySource

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

 

Environment

Environment를 통해서 특정 외부 설정에 종속되지 않고 일관성 있게 key=value 형식의 외부 설정에 접근 가능합니다. (environment.getProperty(key) 형식)

Environment 내부에서 여러 과정을 거쳐 PropertySource에 접근하고, 같은 값이 있을 경우에는 아래와 같은 우선순위로 값을 설정합니다.

  • 우선순위 결정 방법!!
    • 더 유연한 것이 더 우선권을 가집니다. (변경하기 어려운 파일보다, 실행 시 원하는 값을 줄 수 있는 자바 시스템 속성이 더 우선권을 가지게 됩니다. 따라서 우선권은, 파일 < 자바 시스템 속성)
    • 범위가 넓은 것보다 좁은 것이 우선권을 가집니다. (자바 시스템 속성은 해당 JVM 안에서 모두 접근 가능하지만, 커맨드 라인 옵션 인수는 main의 arg를 통해 들어오기 때문에 접근 범위가 더 좁습니다. 따라서 우선권은, 자바 시스템 속성 < 커맨드 라인 옵션 인수)

나중에 설명할 설정 파일(application.properties, application.yml)도 PropertySource에 추가되기 때문에 Environment를 통해 접근 가능합니다.


import jakarta.annotation.PostConstruct;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class EnvironmentProperty {
    private final Environment env;
    public EnvironmentProperty(Environment env) {
        this.env = env;
    }
    @PostConstruct
    public void init() {
        String url = env.getProperty("url");
        String username = env.getProperty("username");
        String password = env.getProperty("password");
        System.out.printf("env url=%s\n", url);
        System.out.printf("env username=%s\n", username);
        System.out.printf("env password=%s\n", password);
    }
}

 

 

위와 같이 Environment 빈을 이용하여 main 함수를 실행하면 어떤 외부 설정을 사용하여도 아래와 같이 동일한 결과가 나옵니다.

  • 커맨드 라인 옵션 인수 실행
    • --url=devdb --username=dev_user --password=dev_pw 
  • 자바 시스템 속성 실행
    • -Durl=devdb -Dusername=dev_user -Dpassword=dev_pw


[참고자료]

김영한, 스프링 부트 - 핵심 원리와 활용", 인프런

 

728x90
반응형