Spring, Apache, Java

외부 설정과 프로필 관리 Part.1

cwchoiit 2024. 6. 14. 15:37
728x90
반응형
SMALL

하나의 애플리케이션을 여러 다른 환경에서 사용해야 할 때가 있다. 대표적으로 개발이 잘 진행되고 있는지 내부에서 확인하는 용도의 개발 환경, 그리고 실제 고객에게 서비스하는 운영 환경이 있다.

 

이렇게 두 환경에서는 서로 다른 데이터와 서로 다른 환경을 사용한다.

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

문제는 각각의 환경에 따라서 서로 다른 설정값이 존재한다는 점인데, 대표적으로 DB만 하더라도 개발 환경에서는 개발DB를 사용하고 개발DB에 접근하려면 dev.db.com 이라는 URL 정보가 필요하고 운영 환경에서 운영DB에 접근하려면 prod.db.com 이라는 URL을 사용해야 한다.

 

이 문제를 해결하는 가장 단순한 방법은 다음과 같이 각각의 환경에 맞게 애플리케이션을 빌드하는 것이다.

  • 개발 환경에는 `dev.db.com`이 필요하므로 이 값을 애플리케이션 코드에 넣은 다음에 빌드해서 개발용 jar 파일을 만든다.
  • 운영 환경에는 `prod.db.com`이 필요하므로 이 값을 애플리케이션 코드에 넣은 다음에 빌드해서 운영용 jar 파일을 만든다.

 

그러나 이 방식은 이러한 문제들이 있다.

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

결론은, 위 방식은 안 좋은 방식이다. 그래서 보통은 다음과 같이 빌드를 한번만 하고 각 환경에 맞추어 실행 시점에 외부 설정값을 주입한다.

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

이런 방식을 사용한다면 빌드도 한번만 하면 되고, 개발 버전과 운영 버전의 빌드 결과물이 같기 때문에 개발 환경에서 검증되면 운영 환경에서도 믿고 사용할 수 있다. 그리고 이후에 새로운 환경이 추가되어도 별도의 빌드 과정 없이 같은 app.jar를 사용해서 손쉽게 새로운 환경을 추가할 수 있다.

유지보수하기 좋은 애플리케이션 개발의 가장 기본 원칙은 변하는 것과 변하지 않는 것을 분리하는 것이다. (DI, 전략 패턴, OCP원칙 등)

 

이제, 이러한 내용을 인지한 채 외부 설정에 대해 공부해보자.

외부 설정

애플리케이션을 실행할 때 필요한 설정값을 외부에서 어떻게 불러와서 애플리케이션에 전달할 수 있을까?

외부 설정은 일반적으로 다음 4가지 방법이 있다.

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

하나씩 차근차근 직접 해보면서 알아가보자. 

 

외부 설정 - OS 환경 변수

우선, OS별 환경 변수가 어떻게 저장되어 있는지 확인하는 방법이 있다. 검색하면 바로 나올거고 Mac은 `printenv`를 입력하면 된다.

 

그럼, 이 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 (Map.Entry<String, String> keyValue : envMap.entrySet()) {
            log.info("key: {}, value: {}", keyValue.getKey(), keyValue.getValue());
        }
    }
}

실행결과

> Task :OsEnv.main()
15:45:14.720 [main] INFO hello.external.OsEnv - key: __CFBundleIdentifier, value: com.jetbrains.intellij
15:45:14.723 [main] INFO hello.external.OsEnv - key: PATH, value: /Users/cw.choiit/.local/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin:/usr/local/go/bin:/Users/cw.choiit/flutter/bin:/Users/cw.choiit/.pub-cache/bin:/Users/cw.choiit/Library/Android/sdk
15:45:14.723 [main] INFO hello.external.OsEnv - key: ANDROID_SDK, value: /Users/cw.choiit/Library/Android/sdk
15:45:14.723 [main] INFO hello.external.OsEnv - key: SHELL, value: /bin/zsh
15:45:14.723 [main] INFO hello.external.OsEnv - key: PAGER, value: less
15:45:14.723 [main] INFO hello.external.OsEnv - key: IJ_RESTARTER_LOG, value: /Users/cw.choiit/Library/Logs/JetBrains/IntelliJIdea2024.1/restarter.log
15:45:14.723 [main] INFO hello.external.OsEnv - key: LSCOLORS, value: Gxfxcxdxbxegedabagacad
15:45:14.723 [main] INFO hello.external.OsEnv - key: JAVA_HOME, value: /Users/cw.choiit/Library/Java/JavaVirtualMachines/openjdk-22.0.1/Contents/Home
15:45:14.724 [main] INFO hello.external.OsEnv - key: OLDPWD, value: /
15:45:14.724 [main] INFO hello.external.OsEnv - key: USER, value: cw.choiit
15:45:14.724 [main] INFO hello.external.OsEnv - key: ZSH, value: /Users/cw.choiit/.oh-my-zsh
...

 

그런데, OS 환경 변수는 이 프로그램 뿐 아니라 다른 곳 어디서도 읽어올 수가 있다. 여러 프로그램에서 사용하는게 맞을 때도 있지만 딱 이 자바 프로그램에만 적용하고 싶을때가 있다. 이렇게 특정 자바 프로그램 안에서만 사용할 수 있는 자바 시스템 속성에 대해 알아보자.

 

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

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

 

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

  • java -Durl=dev -jar app.jar
  • `-D` VM옵션을 통해서 key=value 형시기을 주면 된다.
  • 순서에 주의해야 한다. `-D` 옵션이 -jar 보다 앞에 있다.

그럼 이렇게 실행하면 코드로 어떻게 읽을까? 이것도 간단하다.

JavaSystemProperties

package hello.external;

import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.Properties;

@Slf4j
public class JavaSystemProperties {
    public static void main(String[] args) {
        Properties properties = System.getProperties();
        for (Map.Entry<Object, Object> keyValue : properties.entrySet()) {
            log.info("key = {}, value = {}", keyValue.getKey(), keyValue.getValue());
        }
    }
}

System.getProperties()로 전체 속성을 가져올 수 있다. (타입이 Properties인데 이 PropertiesMap의 자식 타입이다)

실행결과

15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = apple.awt.application.name, value = JavaSystemProperties
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = sun.management.compiler, value = HotSpot 64-Bit Tiered Compilers
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = ftp.nonProxyHosts, value = local|*.local|169.254/16|*.169.254/16|lx.astxsvc.com|*.lx.astxsvc.com|localhost|*.localhost
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = java.runtime.version, value = 18.0.2+0
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = user.name, value = cw.choiit
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = path.separator, value = :
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = os.version, value = 14.5
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = java.runtime.name, value = OpenJDK Runtime Environment
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = file.encoding, value = UTF-8
15:49:42.647 [main] INFO hello.external.JavaSystemProperties - key = java.vm.name, value = OpenJDK 64-Bit Server VM
...

 

사용자가 직접 VM 옵션을 추가해서 그 값을 읽어와보자.

 

JavaSystemProperties

package hello.external;

import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.Properties;

@Slf4j
public class JavaSystemProperties {
    public static void main(String[] args) {
        Properties properties = System.getProperties();
        for (Map.Entry<Object, Object> keyValue : properties.entrySet()) {
            log.info("key = {}, value = {}", keyValue.getKey(), keyValue.getValue());
        }

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

위에서 VM 옵션으로 url, username, password를 추가했다. 그리고 출력해보면 결과는 다음과 같다.

실행결과

...
16:01:27.819 [main] INFO hello.external.JavaSystemProperties - key = sun.io.unicode.encoding, value = UnicodeBig
16:01:27.819 [main] INFO hello.external.JavaSystemProperties - key = socksNonProxyHosts, value = local|*.local|169.254/16|*.169.254/16|lx.astxsvc.com|*.lx.astxsvc.com|localhost|*.localhost
16:01:27.819 [main] INFO hello.external.JavaSystemProperties - key = java.class.version, value = 62.0
16:01:27.819 [main] INFO hello.external.JavaSystemProperties - key = username, value = testuser
16:01:27.819 [main] INFO hello.external.JavaSystemProperties - url = dev
16:01:27.819 [main] INFO hello.external.JavaSystemProperties - username = testuser
16:01:27.819 [main] INFO hello.external.JavaSystemProperties - password = test

 

사용자가 직접 코드로도 자바 시스템 속성을 추가할 수 있다 다만, 이건 코드 내에서 직접 작성하는 것이라서 외부로 설정을 분리하는 효과는 없다. 

다음과 같이 System.setProperty()로 직접 추가가 가능하다.

System.setProperty("Hi", "Hello World");

실행결과

...
16:03:35.282 [main] INFO hello.external.JavaSystemProperties - key = Hi, value = Hello World
...

 

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

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

 

다음과 같이 사용한다.

  • java -jar app.jar dataA dataB
  • 순서에 주의해야 한다. `.jar` 뒤에 스페이스로 구분해서 데이터를 전달한다. 위의 경우 dataA, dataBargs에 전달된다.

자바 시스템 속성이랑 유사하지만 명확히 구분지어야 한다. 

  • `-D`옵션은 `.jar` 앞에, 커맨드 라인 인수는 `.jar` 뒤에

그럼 이렇게 들어오는 인수는 다음과 같이 받으면 된다.

 

CommandLineV1

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

 

실행결과

> Task :CommandLineV1.main()
16:06:53.708 [main] INFO hello.external.CommandLineV1 - dataA
16:06:53.711 [main] INFO hello.external.CommandLineV1 - dataB

 

근데 보면 알겠지만, 이는 `key=value` 형식이 아니다. 애플리케이션 개발하는 입장에서는 명확하게 특정 키에 해당하는 값을 찾는게 편할 때가 많다. 근데 이 경우 통으로 문자열이다. 그래서 만약 저 형식으로 데이터를 `A=dataA B=dataB` 이렇게 넣어도 저 문자열 하나가 그냥 통 문자열이라서 가져다 사용하는 개발자 입장에서는 `=`를 기준으로 데이터를 파싱하고 변환하고 이러한 번거로움이 있다.

 

근데 이를 스프링에서는 편리하게 사용할 수 있도록 스프링 만의 표준 방식을 정의해서 제공해준다. 그것을 알아보자.

 

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

그러니까 스프링은 커맨드 라인 인수를 받을 때 key=value 형식으로 받게 도와준다. 개발자들은 이런 key=value 형식의 데이터를 선호한다. 그래서 스프링이 이렇게 받을 수 있게 도와주는데 그 방법은 `--`이다.

  • java -jar app.jar --username=dev_user --password=dev_pw

이렇게 `--`로 원하는 key=value 형식의 데이터를 전달해주고 받을 수 있다. 스프링이 자체적으로 제공해주는 기능이고 자바의 기본 기능은 아니다.

 

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("args: {}", arg);
        }

        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        log.info("SourceArgs = {}", List.of(applicationArguments.getSourceArgs()));
        log.info("NonOptionArgs = {}", applicationArguments.getNonOptionArgs());
        log.info("OptionsNames = {}", applicationArguments.getOptionNames());

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

        List<String> username = applicationArguments.getOptionValues("username");
        List<String> password = applicationArguments.getOptionValues("password");

        log.info("username = {}", username);
        log.info("password = {}", password);
    }
}

위 코드를 보면, DefaultApplicationArguments 라는 클래스를 사용한다. 여기에 커맨드 라인 인수를 넘겨주면 이 객체가 알아서 파싱을 해준다고 생각하면 된다. 그래서 실제로 위 커맨드처럼 실행을 하면 결과는 다음과 같다.

실행결과

> Task :CommandLineV2.main()
19:17:20.087 [main] INFO hello.external.CommandLineV2 - args: --username=dev_user
19:17:20.091 [main] INFO hello.external.CommandLineV2 - args: --password=dev_pw
19:17:20.104 [main] INFO hello.external.CommandLineV2 - SourceArgs = [--username=dev_user, --password=dev_pw]
19:17:20.104 [main] INFO hello.external.CommandLineV2 - NonOptionArgs = []
19:17:20.106 [main] INFO hello.external.CommandLineV2 - OptionsNames = [password, username]
19:17:20.106 [main] INFO hello.external.CommandLineV2 - option arg password=[dev_pw]
19:17:20.106 [main] INFO hello.external.CommandLineV2 - option arg username=[dev_user]
19:17:20.106 [main] INFO hello.external.CommandLineV2 - username = [dev_user]
19:17:20.106 [main] INFO hello.external.CommandLineV2 - password = [dev_pw]

 

근데 자세히 보면 타입이 List<String>이다. 이 이유는 같은 키로 된 여러개의 값을 전달할 수도 있기 때문이다.

  • java -jar app.jar --username=dev_user --password=dev_pw --username=dev_user2

이렇게 넘기면 배열로 두 개 모두를 받는다.

실행결과

...
19:23:58.534 [main] INFO hello.external.CommandLineV2 - option arg username=[dev_user, dev_user2]
19:23:58.534 [main] INFO hello.external.CommandLineV2 - username = [dev_user, dev_user2]
...

 

근데, 스프링 부트는 또 다르게 이런 방식을 도와준다. 어떻게 도와줄까?

외부 설정 - 커맨드 라인 옵션 인수와 스프링 부트

스프링 부트에서는 저 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));
        }
    }
}

위 코드를 보면 ApplicationArguments를 주입받는다. 그리고 그 녀석을 통해서 값을 똑같이 가져오는 코드를 작성했다. 그리고 돌려보면 실행결과는 다음과 같다.

실행결과

 

이렇게 스프링 부트는 저 객체를 빈으로 자동으로 등록해주고 우리가 어디서든 주입받아 사용할 수 있게 도와준다.

 

결론

이렇게 외부로부터 어떤 데이터(설정값, 변수)를 가져오는 방법을 알아봤다. 모두 자주 사용되는 방식이다.

근데 문제가 있다. 모두 다 값을 가져오는 방식이 천차만별이다. OS 환경 변수값, 자바 시스템 속성값, 커맨드 라인 인수를 가져오는 방법이 모두 다르다. 이럴때 생각해야 하는건 `추상화`다. 인터페이스와 구현(역할과 구현)을 활용해서 스프링은 이 외부로부터 설정값을 가져오는 방식을 통합했다. 그 방법을 Part.2에서 알아보자

728x90
반응형
LIST