이 글에서 다루는 내용

이 글에서는 Spring Boot가 같은 설정 키를 여러 곳에서 발견했을 때 어떤 값을 우선하는지 그 순서를 정리하고, IntelliJ Run/Debug Configuration의 세 가지 입력란(Program arguments, VM options, Environment variables)이 각각 어떤 설정 소스로 연결되는지 살펴봅니다. 이어서 각 입력란의 추천 용도를 구분하고, RevenueCat Webhook Secret 같은 민감정보를 어디에 두어야 하는지, 마지막으로 개발 환경은 IDE 환경변수로 운영 환경은 .env와 systemd로 분리하는 구성에서 주의할 점까지 다룹니다.

Spring Boot 환경 변수 우선순위

같은 설정 키가 여러 위치에 정의되어 있을 때 Spring Boot는 정해진 순서에 따라 어떤 값을 쓸지 결정한다. 우선순위가 높은(이기는) 쪽부터 정리하면 다음과 같다.

  1. 커맨드라인 인수 --key=value
  2. SPRING_APPLICATION_JSON (환경변수나 시스템 프로퍼티로 넘긴 JSON)
  3. Java 시스템 프로퍼티 -Dkey=value
  4. OS 환경변수
  5. application-{profile}.yml (프로파일별, jar 밖이 jar 안보다 우선)
  6. application.yml (기본)
  7. @PropertySource
  8. SpringApplication.setDefaultProperties() 기본값

흔히 커맨드라인 인수, 시스템 프로퍼티, OS 환경변수를 같은 급의 "외부 주입"으로 묶어 생각하기 쉽지만 셋의 우선순위는 분명히 다르다. --key=value(커맨드라인 인수)가 -Dkey=value(시스템 프로퍼티)보다 우선하고, 시스템 프로퍼티는 다시 OS 환경변수보다 우선한다. 즉 같은 키를 세 곳에 모두 정의하면 커맨드라인 인수 값이 최종적으로 이긴다.

Spring Boot 2.4부터 설정 파일 로딩 방식(spring.config.import, 프로파일 처리)이 바뀐 적이 있으므로, 정확한 전체 목록은 사용 중인 버전의 공식 문서 "Externalized Configuration" 섹션을 확인하는 것이 좋다.

IntelliJ Run/Debug Configuration의 세 가지 입력란

IntelliJ에서 값을 주입한다고 해서 무조건 우선순위 1번이 되는 것은 아니다. Run/Debug Configuration 화면의 어느 칸에 넣느냐에 따라 연결되는 설정 소스가 달라진다.

  • Program arguments 칸 → 커맨드라인 인수 --key=value → 우선순위 1번
  • VM options 칸 → 시스템 프로퍼티 -Dkey=value → 우선순위 3번
  • Environment variables 칸 → OS 환경변수 → 우선순위 4번

같은 server.port를 세 칸에 모두 적으면 Program arguments에 적은 값이 이긴다. 자주 하는 실수가 VM options 칸에 --server.port=8081처럼 --를 붙여 적는 것인데, 이러면 시스템 프로퍼티로도 커맨드라인 인수로도 제대로 인식되지 않는다. VM options에는 -D, Program arguments에는 --로 구분해서 넣어야 한다.

각 입력란의 설정 방법

진입 경로는 공통이다. 상단 실행 버튼 옆 드롭다운에서 Edit Configurations를 열고 본인의 Spring Boot 앱을 선택한다. 만약 VM options나 Environment variables 칸이 보이지 않으면 Modify options 링크를 눌러 해당 항목을 체크하면 칸이 나타난다.

Program arguments — 커맨드라인 인수

--(대시 두 개) 접두사를 사용하며 여러 개는 띄어쓰기로 구분한다. 값에 공백이 있으면 따옴표로 묶는다.

--server.port=8081 --spring.profiles.active=local

VM options — 시스템 프로퍼티

-D 접두사를 사용한다. JVM 자체 옵션도 같은 칸에 함께 적지만, -Xmx 같은 JVM 옵션에는 -D를 붙이지 않고 애플리케이션 프로퍼티에만 붙인다.

-Xmx512m -Dspring.profiles.active=local

Environment variables — OS 환경변수

직접 입력할 때는 세미콜론으로 구분한다.

SERVER_PORT=8081;SPRING_PROFILES_ACTIVE=local

여기서 가장 중요한 것은 키 이름 변환 규칙(Relaxed Binding)이다. 환경변수는 점(.)을 쓸 수 없으므로 형식이 다르다.

  • server.portSERVER_PORT
  • spring.datasource.urlSPRING_DATASOURCE_URL
  • my-app.api-keyMYAPP_APIKEY (하이픈은 제거, 점만 언더스코어로 변환)

같은 server.port를 세 칸에 넣는 형식을 비교하면 다음과 같다.

입력 형식
Program arguments --server.port=8081
VM options -Dserver.port=8081
Environment variables SERVER_PORT=8081

각 입력란의 추천 용도

세 칸은 기능이 겹쳐도 관습적으로 굳어진 권장 사용처가 있다.

  • Program arguments — 우선순위가 가장 높고 형식이 단순해서 자주 바꾸는 임시값이나 실행 단위 토글에 적합하다. 포트 변경, 프로파일 잠깐 전환, 기능 플래그 토글 등.
  • VM options — 본업은 JVM 자체 설정이다. 힙 크기(-Xmx, -Xms), GC 설정, -Dfile.encoding=UTF-8, 디버깅용 에이전트 옵션 등. 애플리케이션 프로퍼티를 굳이 여기 넣을 이유는 적다.
  • Environment variables — 운영 환경(서버, 도커, 쿠버네티스) 재현과 민감정보 주입에 쓴다. 커맨드라인 인수나 VM options에 넣은 값은 프로세스 목록에서 노출될 수 있지만 환경변수는 상대적으로 덜 노출된다.

큰 흐름으로 보면 토글성 값은 Program arguments, JVM 튜닝은 VM options, 운영과 맞춰야 하거나 비밀로 둬야 하는 값은 Environment variables로 가는 것이 무난하다.

민감정보는 환경변수로

RevenueCat Webhook Secret처럼 민감한 값은 환경변수로 빼는 것이 정석이다. 이 값은 RevenueCat이 보낸 웹훅이 진짜인지 검증하는 키이므로, 노출되면 제3자가 가짜 결제 완료 웹훅을 위조해 보낼 수 있다. DB 비밀번호급으로 다뤄야 하며 yml에 하드코딩해 git에 올리면 사고로 직결된다.

코드 쪽은 application.yml에 자리만 잡아두고 값은 환경변수에서 끌어오게 한다.

revenuecat:
  webhook-secret: ${REVENUECAT_WEBHOOK_SECRET}

환경변수 키 이름은 relaxed binding 규칙대로 REVENUECAT_WEBHOOK_SECRET으로 두면 revenuecat.webhook-secret에 매핑된다. 추가로 챙기면 좋은 점은 다음과 같다.

  • IntelliJ Run Configuration을 "Store as project file"로 공유 설정해두면 환경변수 값이 .idea/ xml에 저장되어 git에 올라갈 수 있으므로, 공유 설정에는 실제 시크릿을 넣지 않는다.
  • 로컬에서 편하게 관리하려면 .env 파일에 모아두고 .gitignore에 추가하는 방식을 쓸 수 있다. 단 Spring Boot는 .env를 기본으로 읽지 않으므로 EnvFile 플러그인이나 spring-dotenv 같은 라이브러리가 필요하다.
  • 웹훅 서명 검증 시에는 일반 문자열 비교 대신 상수 시간 비교(예: MessageDigest.isEqual)를 쓰면 타이밍 공격을 막을 수 있다.

개발은 IDE 환경변수, 운영은 .env와 systemd

개발 환경은 IDE의 Environment variables로 테스트하고, 운영 환경은 .env를 만들어 systemd가 읽게 하는 구성은 충분히 합리적이며 실무에서 흔히 쓰인다. 다만 systemd가 .env를 읽는 방식에 함정이 있다.

systemd unit에서 EnvironmentFile=/path/to/.env로 지정하면 이 파일은 셸 스크립트가 아니라 systemd 자체 파서가 단순 KEY=VALUE 목록으로 읽는다. 그래서 셸 문법이 통하지 않는다.

  • export KEY=value처럼 export를 붙이면 안 된다. systemd는 export를 키 이름의 일부로 오해한다. 평소 셸에서 source 하려고 만든 .env에는 export가 붙어 있는 경우가 많으니 주의한다.
  • 따옴표 처리가 셸과 다르다. 값에 $, #, 공백, 따옴표 등 특수문자가 들어가면 동작이 미묘하게 달라질 수 있으므로 실제로 읽히는지 확인한다. 변수 확장($OTHER_VAR)도 셸처럼 되지 않는다.

시크릿이 평문으로 들어 있으니 파일 권한도 조여야 한다.

chmod 600 /etc/myapp/.env
chown myapp:myapp /etc/myapp/.env

unit 파일은 대략 다음과 같은 모양이 된다.

[Service]
EnvironmentFile=/etc/myapp/.env
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
User=myapp

이 구성의 핵심 장점은 IDE Environment variables든 systemd EnvironmentFile이든 결국 둘 다 OS 환경변수로 앱에 전달된다는 점이다. 따라서 REVENUECAT_WEBHOOK_SECRET 같은 키 이름만 양쪽에서 동일하게 맞춰두면 코드와 yml은 전혀 손대지 않고 환경만 갈아끼울 수 있다.

규모가 커져 서버가 여러 대가 되거나 시크릿 교체가 잦아지거나 접근 감사가 필요해지면 평문 .env 관리가 번거로워진다. 그때는 systemd의 LoadCredential이나 systemd-creds, 혹은 AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault 같은 전용 시크릿 매니저를 고려할 수 있다. 당장 도입할 필요는 없고 .env가 손에 부치기 시작하면 검토하면 된다.