728x90
반응형
SMALL

외부로부터 설정값들을 가져오는 방법엔 크게 4가지가 있다고 했다. 

  • OS 환경 변수
  • 자바 시스템 속성
  • 커맨드 라인 인수
  • 외부 설정 파일

Part.1에서는 세가지를 배웠다. OS 환경 변수, 자바 시스템 속성, 커맨드 라인 인수.

근데 배우고 보니 저 세가지 방법 모두가 코드에서 가져오는 방식이 다 다르다는 것을 깨달았다. 그럼 여기서 스프링은 이를 두고보지 않는다. 우리에게 추상화 기능을 제공해서 아주 편리하게 어떻게 설정값을 지정했던 상관없이 한 가지 방법으로 모든 방법을 사용할 수 있도록 한다. 다음 그림을 보자.

스프링에서는 커맨드 라인 옵션 인수이던, 자바 시스템 속성이던, OS 환경변수이던, 설정 데이터(파일)이던 상관없이 딱 하나 `Environment` 객체를 통해서 원하는 값을 가져올 수 있다.

 

Environment는 역할(인터페이스)이고 이를 구현한 여러 구현체가 있다. 우리는 그 각각의 세부적인 구현체에 대해 자세히 알 필요없이 그저 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 environment;

    public EnvironmentCheck(Environment environment) {
        this.environment = environment;
    }

    @PostConstruct
    public void init() {
        String url = environment.getProperty("url");
        String username = environment.getProperty("username");
        String password = environment.getProperty("password");

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

그래서 이렇게 스프링이 미리 만들어 둔 Environment를 주입받아서 getProperty()를 호출하면 끝이다.

실행결과

2024-06-14T21:50:32.324+09:00  INFO 43869 --- [           main] hello.EnvironmentCheck                   : url: devurl, username: dev_user, password: dev_pw

 

근데, 한가지 궁금한 부분이 생긴다. 그럼 만약 OS 환경 변수와 자바 시스템 속성 둘 다 또는 그 이상이 모두 같은 key를 가지는 값이 있을땐 무엇을 가져올까? 스프링이 자체적으로 우선순위를 만들어 두었다. 그 우선순위에 대해 알아보자.

 

예를 들어, 다음과 같이 커맨드 라인 옵션 인수자바 시스템 속성으로 같은 키를 지정했다면 어떤 값을 가져올까?

실행결과

url: devurl, username: dev_user, password: dev_pw

 

결과는 커맨드 라인 옵션 인수값을 가져온다. 즉, 자체적으로 우선순위가 커맨드 라인 옵션 인수가 더 높다는 뜻이다. 그럼 이걸 외워야하나?

 

우선순위는 상식 선에서 딱 2가지만 기억하면 된다.

  • 더 유연한 것이 우선권을 가진다 (변경하기 어려운 파일 보다 실행 시 원하는 값을 줄 수 있는 자바 시스템 속성이 더 우선권을 가진다)
  • 범위가 넓은 것보다 좁은 것이 우선권을 가진다(OS 환경 변수처럼 전역으로 여기저기 프로그램에서 가져올 수 있는 값보다 JVM 안에서만 접근 가능한 자바 시스템 속성이 더 우선순위가 높고, JVM 안에서 모두 접근 가능한 경우보다 커맨드 라인 옵션 인수는 main의 args를 통해서 들어오기 때문에 이 커맨드 라인 옵션 인수가 더 우선순위가 높다)

 

이제 스프링이 제공해주는 추상화 `Environment`를 통해 어떻게 외부에 데이터(설정값)를 저장했다고 해도 편리하게 가져다가 사용할 수 있게 됐다. 그럼 남은 한 가지, 외부 파일을 통해 가져오는 것도 알아보자.

 

설정 데이터1 - 외부 파일

지금까지 배운 내용으로 외부로부터 설정 데이터를 가져올 수 있게 됐다. 근데 사실 설정값이 지금이야 3개뿐이니 굉장히 간단하고 편해보이지만 실제 운영서버에서 사용되는 설정 데이터는 몇십개 몇백개도 존재할 수 있다. 그럼 벌써 머리 아프다. 관리하기가 굉장히 난처해진다.

그래서 그렇게 많은 데이터를 관리하기엔 파일로 관리하는게 최고다.

 

그래서 파일로 관리를 하고 애플리케이션 로딩 시점에 해당 파일을 읽어들이면 된다. 그 중에서도 .properties 파일이나 .yml 파일이 key=value 형식으로 설정값을 관리하기에 아주 적합하다.

 

그래서 .jar 파일을 빌드를 통해 만들고 그 .jar 파일이 있는 경로에 application.yml 파일을 만들어보자.

위 사진과 같이 .jar 파일이 존재하는 경로에 application.yml 파일을 만들어서 그 안에 key=value 값을 넣었다.

이 상태에서 애플리케이션을 실행해보자.

보이는 것과 같이 외부 파일을 읽어들여 값을 잘 찍는것을 볼 수 있다. 근데 이렇게 하는 것은 어떤 불편함이 있냐?

서버가 10대라면 10대 서버마다 이러한 파일을 다 만들어야 하는 불편함이 있다. 그리고 설정값이 변경되면 또 10대 모두 다 변경해줘야 한다. 

 

그럼 어떻게 해결할까?

설정 데이터2 - 내부 파일 분리

이 외부 파일을 관리하는 것은 상당히 쉽지 않은 일이다. 설정을 변경할 때 마다 서버에 들어가서 각각의 변경 사항을 수정해두어야 한다. (물론 이것을 자동화 하기 위해 노력을 할 수는 있다) 

 

이런 문제를 해결하는 간단한 방법은 설정 파일을 프로젝트 내부에 포함해서 관리하는 것이다. 그리고 빌드 시점에 함께 빌드되게 하는 것이다. 이렇게 하면 애플리케이션을 배포할 때 설정 파일의 변경 사항도 함께 배포할 수 있다. 쉽게 이야기해서 jar 하나로 설정 데이터까지 포함해서 관리하는 것이다. 

위 그림을 보면 프로젝트 안에 설정 데이터를 포함하고 있다. 

  • 개발용 설정 파일: application-dev.properties
  • 운영용 설정 파일: application-prod.properties
  • 빌드 시점에 개발, 운영 설정 파일을 모두 포함해서 빌드한다.
  • app.jar는 개발, 운영 두 설정 파일을 모두 가지고 배포된다.
  • 실행할 때 어떤 설정 데이터를 읽어야 할지 최소한의 구분은 필요하다.
  • 실행할 때 외부 설정을 사용해서 개발 서버는 dev라는 값을 제공하고, 운영 서버는 prod라는 값을 제공하면 된다. 그리고 스프링에서 이것을 프로필이라고 미리 정의해두고 사용자들에게 제공하고 있다.

"그럼 이 프로필 정보는 어떻게 넘겨요?" 지금까지 했던 커맨드 라인 옵션 인수, VM 옵션 등으로 넘길 수 있다.

우선, 이 내부 설정 파일을 만들어보자. main/resources 경로에 다음 파일을 추가하자.

 

application-dev.yml

username: dev_user
password: dev_password
url: dev.db.com

 

application-prod.yml

username: prod_user
password: prod_password
url: prod.db.com

 

어? 근데 왜 그림에서는 .properties를 사용하는데 여기선 .yml을 사용하나요?

.properties 파일로 사용해도 되고 .yml 파일로 사용해도 된다. 이것도 취향 차이인데, 개인적으로 .yml 파일을 더 선호한다.

그 이유는 크게 2가지가 있다.

  • 들여쓰기를 통한 가시화 증대 (본인 취향)
  • 중복 키가 존재하면 .properties는 가장 마지막에 작성한 값이 적용되는 반면, .yml 파일은 에러를 발생시켜준다.

난 이 두번째 이유가 너무 좋다. 다음 예시를 보자.

application-dev.properties

이 파일에 `url` 이라는 키를 세 개나 중복해서 작성했다. 지금이야 한 눈에 보이지만 이 파일이 꽤나 커져서 위에서 작성했는지 까먹고 아래에서 또 작성할 여지가 분명히 존재한단 말이다. 아마 이 빨간줄은 인텔리제이 유료 버전이 똑똑하게 알려주는 것 같은데 다른 IDE나 기본 파일 에디터는 이런 것도 안 알려줄거다. 

 

이 상태로 실행해보면 결과가 다음과 같다.

가장 마지막에 선언한 값이 적용된다. 뭐 어떻게 보면 유연하다고 볼 수도 있는데 난 이런 유연함은 싫다.

 

application-dev.yml

이 상태로 실행해보자. 다음과 같이 에러가 발생한다. 그리고 친절하게 DuplicateKeyException 이라고 알려준다. 얼마나 좋은가!

 

아무튼 이런 이유로 .yml 파일을 선호하고 .yml 파일로 작성했고 계속 진행해보자.

 

스프링은 이런 곳에서 사용하기 위해 프로필이라는 개념을 지원한다. `spring.profiles.active` 외부 설정에 값을 넣으면 해당 프로필을 사용한다고 판단한다. 그리고 프로필에 따라 다음과 같은 규칙으로 해당 프로필에 맞는 내부 설정 파일을 조회한다.

  • application-{profile}.yml

그래서 실행하는 방법은 대표적으로 다음 두가지와 같다.

  • java -Dspring.profiles.active=dev -jar Xxx.jar (VM 옵션)
  • java -jar Xxx.jar --spring.profiles.active=dev (커맨드 라인 옵션 인수)
한번 더 주의! VM 옵션은 -jar 앞에 작성해야 하고, 커맨드 라인 옵션 인수는 -jar 뒤에 작성해야 한다.

 

실행결과

 

이제 정말 간단하게 설정 데이터를 프로젝트 내에 위치시켜 외부로 빼서 서버마다 설정값이 변경되면 적용해줄 필요도 없고 빌드 파일 자체에 모든것이 담겨있게 됐다. 그러나 인간의 욕심은 끝이 없다. 이제 이 파일이 나뉘어져 있다 보니 각각의 설정값이 어떤 대조점이 있는지 한눈에 보기 어렵다는 점이 있다. 그럼 이건 또 어떻게 해결할까?

설정 데이터3 - 내부 파일 합체

스프링은 또 이 설정 파일을 각각 분리해서 관리하면 한눈에 전체가 들어오지 않는 단점이 있다는 것을 알고 물리적인 하나의 파일 안에서 논리적으로 영역을 구분하는 방법을 제공한다. 그리고 이 방법을 대부분 실무에서 많이 사용하고 있다. 

대부분 실무에서 많이 사용하고 있다는 말은 통상적이지 절대적은 아니다. 파일을 application-prod.yml, application-dev.yml로 구분하는 방식도 많이 사용한다. 난 오히려 이게 더 편하기도 하다. 약간 취향의 차이로 생각하면 좋을 것 같다.

 

그럼 이제 어떻게 하면 될까? 위에서는 이런식으로 구분을 했다.

 

application-dev.yml

username: dev_user
password: dev_password
url: dev.db.com

 

application-prod.yml

username: prod_user
password: prod_password
url: prod.db.com

 

이 파일을 하나로 합쳐보자. 그리고 스프링이 제공하는 .yml 구분자(---)로 두 프로필을 구분해보자.

application.yml

spring:
  config:
    activate:
      on-profile: dev

username: dev_user
password: dev_password
url: dev.db.com

---

spring:
  config:
    activate:
      on-profile: prod

username: prod_user
password: prod_password
url: prod.db.com

 

이렇게 하면 끝이다. 한 파일로 두 프로필을 논리적으로 구분해서 관리할 수 있다. 

이제 이게 가장 통상적으로 많이 사용되는 방식이고, 어지간하면 이렇게 사용하면 된다. 

 

그럼, 위 설정 파일은 프로필이 반드시 주어져야 정상적으로 동작할 것 처럼 보인다. 맞다. 그럼 프로필을 주지 않으면 어떤 현상이 일어날까? 여기서도 우선순위가 존재하는데 그 부분을 알아보자.

우선순위 - 설정 데이터

자, 만약 내가 프로필을 따로 주지 않고 실행하면 이 상태에선 어떤 값이 적용될까?

application.yml

spring:
  config:
    activate:
      on-profile: dev

username: dev_user
password: dev_password
url: dev.db.com

---

spring:
  config:
    activate:
      on-profile: prod

username: prod_user
password: prod_password
url: prod.db.com

 

실행결과

 

스프링은 프로필을 따로 받지 않을때 "default"라는 기본 프로필을 사용한다. 그리고 결과로 보면 알 수 있듯 어떤 값도 적용되지 않았다.

쉽게 말해 이 설정 프로필에 우선순위란게 존재하지 않는다. 그럼 어떻게 읽어들일까?

 

"위에서 아래로 읽어들인다"

 

다음과 같이 작성했다고 가정해보자.

application.yml

username: local_user
password: local_password
url: local.db.com

---

spring:
  config:
    activate:
      on-profile: dev

username: dev_user
password: dev_password
url: dev.db.com

--- # .yml 파일에서 구분자 (스프링이 직접 정의한 것이기 때문에 반드시 이 '---' 여야 한다.)

spring:
  config:
    activate:
      on-profile: prod

username: prod_user
password: prod_password
url: prod.db.com

제일 상단에는 어떠한 프로필도 적용시키지 않았다. 이런 경우 기본값(default) 프로필로 적용이 된다.

그럼, 스프링은 이 상태에서 아무런 프로필도 받지 않으면 당연히 저 위에 세 개가 적용된다.

 

근데 만약, 프로필을 'dev'로 받는다면 어떤식으로 적용되냐? 다음 순서대로 적용된다.

  • 먼저 기본값인 가장 상위 세 개가 적용된다.
  • 근데, 프로필이 'dev' 이므로 'dev' 전용 설정 값을 적용한다. (여기서 같은 키라면 그 값이 대체된다)

한번 'dev'로 적용해서 실행해보자.

실행결과

 

결과를 보면 알 수 있듯 'dev'값으로 적용됐다. 이건 가장 상위에 세 개를 건너 뛴 게 아니다. 먼저 가장 상위에 세 개를 적용한 후 `dev`라는 프로필을 받았으니 그 프로필에 한하여 적용되는 설정값을 추가적으로 적용한 결과다. 다만 같은 키가 있기에 대체된 것.

 

극단적인 예시로 가장 하위에 아무런 프로필이 없는 값을 주면? 대체된다.

application.yml

spring:
  config:
    activate:
      on-profile: dev

username: dev_user
password: dev_password
url: dev.db.com

--- # .yml 파일에서 구분자 (스프링이 직접 정의한 것이기 때문에 반드시 이 '---' 여야 한다.)

spring:
  config:
    activate:
      on-profile: prod

username: prod_user
password: prod_password
url: prod.db.com

--- 

url: local.db.com

이렇게 가장 하위에 기본 프로필에 적용되는 값을 주면 그 값은 무조건 적용이 된다.

실행결과

그래서, 보통은 가장 상위에 기본으로 적용될 값을 모두 세팅해 둔 후에 각 프로필마다 변경되거나 추가적으로 바뀔 값을 지정하는 방식으로 많이 사용한다.

 

심지어, 프로필을 두개도 적용할 수 있다. 거의 이럴일은 없지만 이렇게 실행하면 진짜 두개의 프로필이 모두 활성화된다.

  • java -Dspring.profiles.active=dev,prod -jar Xxx.jar

실행결과

결과를 보면 알 수 있듯 두개의 프로필이 활성화됐다. 그럼 위에서 말한것처럼 위에서 아래로 읽어 들이는 설정 파일은 결국 'dev'도 읽고 'prod'도 읽으니까 최종적으로 url, username, password의 값은 'prod'값으로 적용이 될 수 밖에 없다는 것을 안다.

 

결론과 우선순위 - 전체

이제 전체적으로 외부 설정을 읽어 들일 때 우선순위가 어떻게 적용되는지 확인해보자.

아래로 내려갈수록 우선순위가 높은것이다.

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

이걸 외우는게 아니라, 그냥 두가지 큰 틀을 알고 있으면 된다.

  • 더 유연한 것이 우선권을 가진다. (변경하기 어려운 파일보다 실행 시 원하는 값을 줄 수 있는 자바 시스템 속성이 더 우선권을 가진다)
  • 범위가 넓은 것보다 좁은 것이 더 우선권을 가진다. (다른 말로, 더 디테일한게 우선권을 가진다)
    • OS 환경변수보다 자바 시스템 속성이 더 우선권을 가진다.
    • 자바 시스템 속성보다 커맨드라인 옵션 인수가 더 우선권을 가진다.

 

728x90
반응형
LIST
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

+ Recent posts