Spring, Apache, Java

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

cwchoiit 2024. 6. 14. 22:02
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