신규 서비스 배포를 앞두고 운영팀에서 연락이 왔습니다. "스테이징에서는 잘 됐는데 운영에서 오류가 납니다." 로그를 열어보니 운영 서버가 개발 DB에 연결 시도하고 있었습니다. 개발자가 application.yml 하나에 모든 환경 설정을 때려넣고, 프로파일 지정 없이 배포했던 것입니다.
이 모듈은 그런 사고를 막기 위한 것입니다. 환경별 설정 파일을 어떻게 분리하고, 민감한 정보를 어디에 두어야 하며, 운영 서버에서 설정을 어떻게 적용하는지를 다룹니다.
- 1application-dev.yml / application-stg.yml / application-prod.yml 파일 분리 구조를 설명할 수 있다
- 2OS 환경변수 > JVM 옵션 > properties 파일의 우선순위를 이해하고 활용할 수 있다
- 3.env 파일, 환경변수, Vault의 용도 차이를 설명하고 .gitignore에 포함할 파일을 알 수 있다
- 4Tomcat setenv.sh로 JVM 환경변수를 주입하는 방법을 실습할 수 있다
- 5설정 변경 후 무중단 적용 여부를 판단하고 올바른 재기동 절차를 선택할 수 있다
환경 분리 — 왜 파일을 나눠야 하나
환경별 설정 파일 분리
개발/스테이징/운영은 DB 주소, 로그 레벨, API 엔드포인트가 모두 다릅니다. 하나의 파일에 모든 환경 설정을 넣으면 배포 실수가 났을 때 어느 환경이 잘못 연결됐는지 추적하기 어렵고, 민감한 운영 정보가 개발 환경에 노출될 위험도 생깁니다.
확대
Spring Boot는 application-{profile}.yml 규칙으로 환경별 파일을 자동으로 읽습니다.
src/main/resources/ 구조입니다.
application.yml— 공통 설정 (프로파일 무관하게 항상 로드)application-dev.yml— 개발 환경 전용application-stg.yml— 스테이징 환경 전용application-prod.yml— 운영 환경 전용
# application.yml (공통)
spring:
application:
name: my-service
jpa:
open-in-view: false
logging:
pattern:
console: "%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb_dev
username: dev_user
password: dev_pass # 개발 환경은 .env나 로컬 파일 허용
jpa:
show-sql: true # 개발에서만 SQL 로그 출력
logging:
level:
root: DEBUG
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME} # 환경변수로 주입
username: ${DB_USER}
password: ${DB_PASSWORD}
jpa:
show-sql: false # 운영에서는 SQL 로그 off
logging:
level:
root: WARN
핵심 원칙: 운영 설정 파일(application-prod.yml)에는 실제 비밀값을 절대 하드코딩하지 않습니다. ${환경변수명} 플레이스홀더만 두고, 실제 값은 OS 환경변수나 Vault에서 주입합니다.
설정 우선순위 — 어디서 값이 오는가
Spring Boot 설정 우선순위
같은 키가 여러 곳에 정의되어 있을 때 어떤 값이 적용되는지 알아야 설정 충돌을 추적할 수 있습니다. Spring Boot는 명확한 우선순위를 따릅니다.
설정 우선순위(위가 가장 강함):
- OS 환경변수 (
SPRING_DATASOURCE_URL) - JVM 시스템 옵션 (
-Dspring.datasource.url=...) - 프로파일 yml 파일 (
application-prod.yml) - application.yml (공통 파일)
OS 환경변수가 가장 강하다는 것이 핵심입니다. 파일에 값이 있더라도 환경변수를 설정하면 그 값이 우선 적용됩니다. 운영 서버에서 DB 비밀번호를 파일에 넣지 않고 환경변수로 주입하는 이유가 여기 있습니다.
확대
# 운영 서버에서 환경변수 확인
env | grep -E "SPRING|DB|APP|JAVA"
# 실행 중인 JVM 프로세스의 환경변수 확인 (PID는 ps aux로 찾기)
cat /proc/$(pgrep -f myapp.jar)/environ | tr '\0' '\n' | grep -E "DB|SPRING"
설정값 하나가 최종 적용되기까지 — 로드부터 병합까지 5단계
application-prod.yml에 DB 주소를 분명히 새 값으로 고쳤는데 앱은 옛 주소에 붙습니다. 파일도 맞고 문법 오류도 없습니다. 이런 일이 나는 건, 앱이 값을 한 곳에서 읽는 게 아니라 여러 계층을 순서대로 읽어 덮어쓰며 하나의 최종값으로 병합하기 때문입니다. 어느 계층이 마지막에 값을 얹었는지 모르면 "고쳤는데 왜 그대로지"를 영원히 못 풉니다. 설정값 하나가 기동 시점에 어떻게 실려 최종 적용값이 되는지를 단계로 봅니다.
[앱 기동] 같은 키(예: spring.datasource.url)를 아래 순서로 읽어 뒤가 앞을 덮는다
│
① 기본값 코드에 박힌 기본값(@Value 기본·프레임워크 디폴트)
│ → 아무 설정도 없을 때 쓰이는 최후의 바닥값
│
② 설정 파일 application.yml (프로파일 무관 공통값)
│
③ 프로파일 파일 application-{active}.yml (dev/stg/prod 중 활성 프로파일)
│ → SPRING_PROFILES_ACTIVE 가 어느 파일을 열지 결정
│
④ 환경변수 OS 환경변수(SPRING_DATASOURCE_URL 등)가 파일 값을 덮음
│
⑤ 커맨드라인/시크릿 --spring.datasource.url=... 실행인자·시크릿 주입 (가장 강함)
▼
[최종 병합값] 각 키마다 '가장 마지막에 얹힌 값'이 실제로 적용된다
각 단계가 무엇을 얹고, 그 계층이 범인일 때의 증상:
| 단계 | 무엇을 얹는가 | 이 계층이 범인이면 증상 |
|---|---|---|
| ① 기본값 | 코드·프레임워크 내장 디폴트 | 위 어느 계층도 값을 안 주면 의도치 않게 로컬(localhost) 기본값으로 뜬다 |
| ② application.yml | 환경 무관 공통값 | 공통 파일에 개발 DB 주소가 남아 있으면 프로파일 미지정 시 그 값으로 붙음 |
| ③ 프로파일 파일 | 활성 프로파일 전용값 | SPRING_PROFILES_ACTIVE 누락 → prod 파일이 안 열려 공통값·개발값이 적용 |
| ④ 환경변수 | 서버별 실제 주입값 | 파일을 고쳐도 옛 값이 든 환경변수가 남아 파일을 덮음(우선순위 역전) · 반대로 누락되면 ${} 미치환 |
| ⑤ 커맨드라인/시크릿 | 최종 오버라이드·비밀값 | 시크릿 미주입이면 비밀번호 자리가 비어 인증 실패 · -D 옛 값이 남으면 모든 걸 덮어 이김 |
즉 최종 적용값은 "가장 마지막에 얹은 계층"이 이깁니다. "고쳤는데 반영이 안 된다"는 대개 ④·⑤의 더 강한 계층에 옛 값이 남아 파일을 덮는 우선순위 역전이고, "설정이 통째로 개발값"이면 ③의 프로파일 미지정입니다. 그래서 진단은 파일부터 보지 말고 강한 계층부터 역순으로 봅니다 — env | grep SPRING, cat /proc/PID/environ, ps aux의 -D 옵션, 그리고 앱에 있으면 actuator/env로 어느 소스가 그 키를 최종 채택했는지를 바로 확인합니다.
민감 정보 관리 — .env, 환경변수, Vault
비밀값을 어디에 두어야 하나
DB 비밀번호, API 키, JWT 시크릿 같은 민감 정보는 코드와 분리해야 합니다. 규모와 보안 요구에 따라 세 가지 방법 중 하나를 선택합니다.
| 방법 | 적합한 상황 | 주의사항 |
|---|---|---|
.env 파일 | 개발 로컬 / 소규모 스테이징 | git에 절대 커밋 금지, .gitignore 필수 |
| OS 환경변수 | 단일 서버 운영 환경 | setenv.sh 또는 systemd 서비스 파일에 주입 |
| HashiCorp Vault | 멀티 서버/클라우드/보안 규정 있는 운영 | 별도 Vault 서버 구성 필요, 접근 권한 관리 |
# .env 파일 예시 (로컬 개발용)
DB_HOST=localhost
DB_NAME=mydb_dev
DB_USER=dev_user
DB_PASSWORD=dev_secret_password
APP_SECRET_KEY=localdev_secret_key_only
.gitignore 에 반드시 포함할 파일들:
# .gitignore
.env
.env.*
!.env.example # 예시 파일(실제 값 없음)은 허용
application-prod.yml # 운영 설정에 비밀이 있을 경우
*.properties.bak
/config/secrets/
# .env.example (저장소에 포함하는 템플릿)
DB_HOST=localhost
DB_NAME=
DB_USER=
DB_PASSWORD= # 팀원이 이 파일을 복사해 .env 를 만든다
APP_SECRET_KEY=
확대
Tomcat 환경변수 주입 — setenv.sh
setenv.sh 로 JVM 환경변수 설정
Tomcat은 기동 시 $CATALINA_HOME/bin/setenv.sh 파일이 있으면 자동으로 실행합니다. 이 파일에 환경변수와 JVM 옵션을 설정하면 Tomcat 프로세스 전체에 적용됩니다. 파일이 없으면 직접 만들면 됩니다.
# /opt/tomcat/bin/setenv.sh
export JAVA_OPTS="
-server
-Xms512m
-Xmx2048m
-Dspring.profiles.active=prod
-Dspring.config.location=/etc/myapp/application-prod.yml
"
# 민감 정보는 OS 환경변수에서 읽어서 JVM에 전달
export JAVA_OPTS="$JAVA_OPTS
-DDB_HOST=${DB_HOST}
-DDB_PASSWORD=${DB_PASSWORD}
"
# setenv.sh 적용 확인 — Tomcat 재기동 후
ps aux | grep java | grep spring.profiles.active
# 또는 실행 중인 프로세스 JVM 옵션 확인
jps -v | grep Catalina
setenv.sh 권한 설정:
chmod 750 /opt/tomcat/bin/setenv.sh
chown tomcat:tomcat /opt/tomcat/bin/setenv.sh
실습
실행 중인 환경에서 어떤 설정 관련 환경변수가 있는지 확인합니다. 운영 서버라면 DB 연결 정보, 프로파일 설정 등이 나와야 합니다. 아무것도 나오지 않는다면 환경변수 주입이 누락된 상태입니다.
# 실행 중인 Java 프로세스의 환경변수 직접 확인
# 먼저 PID 찾기
ps aux | grep java | grep -v grep | awk '{print $2}'
# PID 로 환경변수 읽기 (예: PID=12345)
cat /proc/12345/environ | tr '\0' '\n' | grep -E "DB|SPRING|APP"
SPRING_PROFILES_ACTIVE=prod
DB_HOST=db-prod.internal
DB_NAME=mydb_prod
DB_USER=app_user
env | grep -E 'JAVA|SPRING|DB|APP'- ps aux | grep java에서 -Dspring.profiles.active 값을 먼저 확인 — prod가 없으면 기본 application.yml만 로드되어 개발 DB에 연결될 수 있음. 값이 없으면 setenv.sh 설정 누락
- git ls-files .env 결과가 한 줄이라도 있으면 민감 정보가 저장소에 커밋된 것 — 즉시 git rm --cached .env 실행 후 .gitignore에 추가. 이미 커밋됐으면 히스토리 전체 정리 필요
- -Dspring.profiles.active=prod이고 DB_HOST가 운영 주소인데 연결 오류가 나면 — 환경변수 이름이 맞는데 값만 틀린 경우. printenv | grep DB로 실제 주입된 값을 반드시 재확인
Tomcat을 사용하는 서버라면 setenv.sh에서 어떤 프로파일이 지정됐는지 확인합니다. spring.profiles.active가 없거나 dev로 돼 있으면 운영에서 개발 설정이 적용됩니다.
# setenv.sh 가 없으면 확인
ls -la /opt/tomcat/bin/setenv.sh
# JVM 옵션에서 프로파일 확인
grep -E "profiles|JAVA_OPTS" /opt/tomcat/bin/setenv.sh
# 실행 중인 프로세스에서 직접 확인
ps aux | grep "[Dd]spring.profiles.active"
export JAVA_OPTS="-server -Xms512m -Xmx2048m -Dspring.profiles.active=prod"
cat /opt/tomcat/bin/setenv.sh- setenv.sh에서 -Dspring.profiles.active=prod 줄을 먼저 찾는다 — 없으면 프로파일 미지정 상태. JAVA_OPTS와 CATALINA_OPTS 두 변수 모두 확인해야 함(한쪽에만 있는 경우 많음)
- JAVA_OPTS에 DB 비밀번호가 직접 문자열로 있으면 — ps aux 출력에 그대로 노출됨. 다른 계정 사용자도 비밀번호를 볼 수 있어 보안 위반. 환경변수 참조(${DB_PASSWORD}) 방식으로 즉시 교체 필요
- setenv.sh 파일 권한이 644이면 다른 계정도 읽기 가능 — 비밀번호 등 민감 정보가 있는 경우 750(tomcat 그룹만 실행)이나 700(소유자만)으로 변경 필요. ls -la로 최종 확인
트러블슈팅
원인: Spring Boot 프로파일이 지정되지 않으면 기본 application.yml만 로드됩니다. 개발 DB 주소가 기본 파일에 있거나, setenv.sh에 -Dspring.profiles.active 설정이 누락된 경우입니다.
# 1. 실행 중인 프로세스의 프로파일 확인
ps aux | grep java | grep profiles.active
# 아무것도 안 나오면 프로파일 미지정 상태
# 2. 어떤 설정 파일이 로드됐는지 애플리케이션 로그에서 확인
grep -E "profiles|config.*loaded|active profile" /opt/tomcat/logs/catalina.out | head -20
# 3. setenv.sh 수정 — 프로파일 지정 추가
vi /opt/tomcat/bin/setenv.sh
# 아래 줄 추가
# export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=prod"
# 4. Tomcat 재기동
systemctl restart tomcat
# 5. 재기동 후 확인
grep "The following profiles are active" /opt/tomcat/logs/catalina.out | tail -3
예방: 배포 체크리스트에 "setenv.sh 프로파일 확인" 항목을 추가하고, 배포 스크립트에서 기동 후 로그로 프로파일을 검증합니다.
원인: Java 애플리케이션은 JVM 기동 시점에 설정을 읽어 메모리에 올립니다. 파일을 수정해도 실행 중인 JVM에는 변경이 반영되지 않습니다.
# 설정 파일 수정 후 현재 적용된 값 확인 (Spring Actuator가 있는 경우)
curl http://localhost:8080/actuator/env | python3 -m json.tool | grep -A3 "datasource.url"
# 파일이 수정됐지만 프로세스는 구 값을 갖고 있음을 확인
# /proc/PID/environ 은 기동 시점 환경변수를 반영 → 변경 없음
# 일부 앱은 HUP 시그널로 재로드 지원 (NGINX 등) — Java 앱은 일반적으로 미지원
# kill -HUP $(pgrep -f myapp.jar) ← 대부분의 Spring 앱에서 동작 안 함
# 올바른 방법: 점검 후 재기동
systemctl stop myapp
# 또는 Tomcat이라면
systemctl stop tomcat
# 설정 변경 확인
grep "datasource.url" /etc/myapp/application-prod.yml
# 재기동
systemctl start myapp
systemctl status myapp
무중단이 필요한 경우: 로드밸런서에서 해당 인스턴스를 제외(Drain) → 재기동 → 헬스체크 통과 후 복귀 순서로 처리합니다.
설정 파일 백업 전략
설정 파일 변경 이력 관리
운영 서버의 설정 파일은 변경 전 반드시 백업합니다. 잘못 수정했을 때 빠르게 복구하기 위해서입니다.
# 수정 전 백업 (날짜 포함)
cp /etc/myapp/application-prod.yml \
/etc/myapp/application-prod.yml.$(date +%Y%m%d_%H%M%S).bak
# 백업 목록 확인
ls -lt /etc/myapp/*.bak | head -5
# 이전 버전으로 복구
cp /etc/myapp/application-prod.yml.20260530_143022.bak \
/etc/myapp/application-prod.yml
# 설정 변경 이력은 git으로도 관리 (비밀값 제외한 템플릿)
# 민감 정보가 없는 application-prod.yml.template 을 저장소에 유지
심화 — 주입은 했는데: 시크릿과 우선순위의 뒷면
심화: 환경변수 주입의 한계와 그다음 — 시크릿 매니저
비밀값을 이미지·파일에서 빼 환경변수로 옮기면 큰 진전이지만, "환경변수면 안전하다"에서 멈추면 여전히 새는 구멍이 남습니다. 환경변수가 실제로 어디까지 보이는지 알아야 다음 단계를 판단할 수 있습니다.
- 커맨드라인보다 환경변수가 낫습니다: 비밀값을
-DDB_PASSWORD=...처럼 JVM 커맨드라인에 두면ps aux·/proc/PID/cmdline으로 같은 서버의 다른 사용자에게도 그대로 보입니다. 같은 값이라도 환경변수로 주면 커맨드라인 노출은 사라집니다. - 환경변수도 완전히 숨지는 않습니다: 환경변수는 그 프로세스가 실행한 자식 프로세스에 상속되고, root·소유자는
/proc/PID/environ으로 읽을 수 있으며, 힙 덤프·에러 스택트레이스·잘못 열린actuator/env엔드포인트로 새어 나갈 수 있습니다. - 다음 단계는 시크릿 매니저: 규정이 엄격하거나 서버가 많아지면 HashiCorp Vault·클라우드 Secrets Manager로 넘어갑니다. 값을 디스크에 두지 않고 기동 시 받아오며, 짧은 수명(lease)·자동 회전·접근 감사를 제공합니다. DB 자격증명을 요청 시마다 동적으로 발급해, 유출돼도 곧 만료되게 할 수도 있습니다.
- 한계/원칙: 도구가 무엇이든 원칙은 같습니다 — 비밀은 코드·이미지와 분리, 최소 권한, 그리고 정기 회전. 한 번 유출된 비밀은 코드에서 지워도 이미 복제됐다고 보고 회전(교체)해야 진짜로 닫힙니다.
그래서 "비밀을 환경변수로 뺐다"는 끝이 아니라 시작이고, 다음 질문은 "이 값을 어떻게 짧게 살게 하고, 언제 회전할까"입니다.
상황: 설정 파일을 새 주소로 바꾸고 재기동까지 했는데, 앱 로그의 실제 연결 대상은 옛 주소입니다. 파일을 다시 열어봐도 새 값이 맞고, yml 문법 오류도 없습니다.
원인: Spring Boot는 같은 키를 여러 소스에서 읽되 정해진 우선순위로 합칩니다. OS 환경변수와 -D JVM 옵션은 프로파일 yml·application.yml보다 강합니다. 어딘가(대개 setenv.sh의 JAVA_OPTS나 배포 스크립트가 심어둔 SPRING_DATASOURCE_URL 환경변수)에 옛 값이 남아, 파일을 아무리 고쳐도 그 강한 계층이 파일 값을 덮고 있는 것입니다.
진단: env | grep -E "SPRING|DB"와 cat /proc/PID/environ | tr '\0' '\n' | grep SPRING으로 실제 주입된 환경변수를 보고, setenv.sh의 JAVA_OPTS·CATALINA_OPTS에서 -Dspring.datasource.url 같은 옵션이 남아 있는지 확인합니다. 앱에 actuator/env가 있으면 그 키를 어느 소스가 최종 채택했는지를 바로 볼 수 있습니다.
해결: 옛 값을 쥐고 있는 강한 계층을 지우거나 새 값으로 갱신합니다(환경변수 unset·setenv.sh 수정). 그다음 재기동하면 파일 값이 살아납니다. 재발 방지로 "환경별 차이는 한 계층에서만 관리"라는 규칙을 정해, 같은 키를 파일과 환경변수 양쪽에 흩뿌리지 않습니다.
실제 업무에서 이 지식이 쓰이는 상황:
인프라 엔지니어가 환경 설정 관리에서 가장 자주 마주치는 상황 세 가지입니다.
1. 신규 서버 환경 구성 시:
운영 서버를 새로 세팅할 때 개발자가 만든 application.yml 그대로 올리면 안 됩니다. setenv.sh에 SPRING_PROFILES_ACTIVE=prod를 설정하고, DB 접속 정보는 OS 환경변수로 주입하는 것이 기본 체크리스트입니다.
2. 설정 변경 요청 처리:
# 인프라 엔지니어의 설정 변경 표준 절차
# 1. 현재 설정 백업
cp /opt/tomcat/bin/setenv.sh /opt/tomcat/bin/setenv.sh.$(date +%Y%m%d).bak
# 2. 변경
vi /opt/tomcat/bin/setenv.sh
# 3. 적용을 위한 재기동 (무중단 필요 시 LB에서 먼저 제외)
systemctl restart tomcat
# 4. 적용 확인
sleep 10
curl -s http://localhost:8080/actuator/health | python3 -m json.tool
grep "profiles are active" /opt/tomcat/logs/catalina.out | tail -3
3. 보안 감사 대응: Git 저장소에 비밀번호가 커밋됐는지 점검 요청이 오면 아래를 씁니다.
# 저장소에 .env 파일이 들어간 적 있는지 확인
git log --all --full-history -- .env
git log --all --full-history -- "**/*.properties"
설정 관리를 잘 하는 팀은 배포 사고의 절반을 예방합니다.
명령어·단축키 빠른 참조
이 모듈에서 다룬 환경변수·프로파일 확인과 설정 감사 명령을 실전 옵션과 함께 모았습니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
env | grep | 주입된 설정 환경변수 확인 | env | grep -E "SPRING|DB" |
printenv | 특정 환경변수 값 재확인 | printenv | grep DB |
cat /proc/<PID>/environ | 실행 중 프로세스의 실제 환경변수 | cat /proc/<PID>/environ | tr '\0' '\n' |
pgrep -f <앱> | 앱 PID 찾기 | pgrep -f myapp.jar |
ps aux | grep profiles.active | 활성 프로파일 확인 | -Dspring.profiles.active=prod 있는지 |
jps -v | JVM 기동 옵션 전체 확인 | jps -v | grep Catalina |
curl actuator/env | 최종 채택된 설정값 소스 확인 | curl localhost:8080/actuator/env |
git ls-files .env | 비밀 파일 커밋 여부 점검 | 한 줄이라도 나오면 즉시 조치 |
git rm --cached .env | 추적 제거(파일은 유지) | 이후 .gitignore에 추가 |
git log --all --full-history | 비밀 파일 커밋 이력 감사 | git log --all --full-history -- .env |
chmod 750 setenv.sh | setenv.sh 권한 잠금 | 644면 다른 계정이 비밀 열람 |
systemctl restart tomcat | 설정 반영 재기동 | 무중단은 LB Drain 후 재기동 |
관련 모듈로 더 깊이:
- Git/GitLab 브랜치 전략과 릴리즈 관리 — 설정 변경 이력을 체계적으로 관리하는 Git 형상관리
- 계정 권한 관리, 보안 헤더, TLS 강화 실무 — 비밀번호·키 같은 민감 설정의 보안 관리와 감사
다음 모듈에서는 이런 설정 변경 이력을 체계적으로 관리하는 Git 형상관리 실무를 다룹니다.