환경마다 다른 설정을 어떻게 깔끔하게 관리할 것인가? 에 대한 고민을 정리해봤습니다.
Spring Boot 프로젝트를 처음 시작하면 application.yaml 하나에 모든 설정을 넣게 된다.
그런데 개발을 진행하다 보면 자연스럽게 이런 상황이 생긴다. (특히 팀 단위 개발시)
localhost:3306에 DB가 있는데, 배포 서버에는 10.0.0.5:3306이 있다.prod(운영) DB 주소를 로컬에서 그대로 연결해버린 사고가 발생한다.이 문제를 해결하는 것이 Spring Profile 기반 설정 분리다.
Spring은 spring.profiles.active 값에 따라 어떤 설정 파일을 로드할지 결정한다.
src/main/resources/
├── application.yaml # 공통 설정 (항상 로드됨)
├── application-local.yaml # 로컬 개발 환경 (내 PC, Docker Compose)
├── application-dev.yaml # 개발 서버 (팀 공유 EC2 dev 서버 등)
├── application-prod.yaml # 운영 서버 (실제 배포 환경)
└── application-test.yaml # 테스트 환경 (@SpringBootTest 등)
Spring Boot는 공통 설정(application.yaml)을 먼저 로드하고, 이후 활성화된 프로필 파일을 덮어쓴다(override).
즉, 프로필 파일에 명시된 값이 공통 설정보다 우선 적용된다.
application.yaml (공통) → application-{profile}.yaml (프로필별 덮어쓰기)
application.yaml — 공통 설정모든 환경에서 동일하게 적용되는 값들을 넣는다.
# 공통으로 사용하는 서버 기본 설정
server:
port: 8080
# Spring 공통 설정 (기본 활성 프로필을 local로 지정)
spring:
application:
name: my-service
profiles:
active: local # 로컬 기본값 (배포 시에는 환경변수로 덮어씀)
# 또는 local 실행 -> IDE profile
# dev/prod -> 환경변수로
# 로깅 공통 설정
logging:
level:
root: INFO
주의:
spring.profiles.active를application.yaml에 명시하면 기본 프로필이 된다. 실제 배포 시에는 환경변수SPRING_PROFILES_ACTIVE=prod또는 JVM 인자--spring.profiles.active=prod로 덮어쓴다.
default profile을 코드에 넣지 않는 팀도 있음. 중요한 건 팀 컨벤션을 따르는 것
application-local.yaml — 로컬 개발 환경# 로컬 DB 연결 (Docker Compose로 띄운 DB 등)
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: root
# 로컬에서 DDL 자동 생성 허용 (개발 편의용)
jpa:
hibernate:
ddl-auto: create-drop
# Redis 로컬 주소
data:
redis:
host: localhost
port: 6379
# 로컬에서는 디버그 로깅 활성화
logging:
level:
com.example: DEBUG
application-dev.yaml — 팀 공유 개발 서버# 팀 공유 개발 서버 DB
spring:
datasource:
url: jdbc:mysql://dev-db.internal:3306/mydb
username: devuser
password: ${DB_PASSWORD} # 환경변수로 주입 (민감 정보는 직접 쓰지 않는다)
jpa:
hibernate:
ddl-auto: validate # dev에서는 스키마 검증만
data:
redis:
host: dev-redis.internal
port: 6379
logging:
level:
com.example: DEBUG
application-prod.yaml — 운영 서버# 운영 DB (민감 정보는 반드시 환경변수 또는 외부 시크릿 매니저로)
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/mydb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: none # 운영에서는 절대 자동 DDL 금지
data:
redis:
host: ${REDIS_HOST}
port: 6379
# 운영에서는 불필요한 로그 최소화
logging:
level:
root: WARN
com.example: INFO
application-test.yaml — 테스트 환경# 테스트용 인메모리 DB (H2)
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
database-platform: org.hibernate.dialect.H2Dialect
# 외부 API는 테스트에서 Mock으로 대체하므로 실제 주소 불필요
상황에 따라 다양한 방법으로 활성화할 수 있다.
# 1. JVM 실행 인자 (jar 실행 시)
java -jar app.jar --spring.profiles.active=prod
# 2. 환경변수 (Docker, Linux 서버 등에서 주로 사용)
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar
# 3. IntelliJ 실행 구성
# Run/Debug Configurations > Active profiles > "local" 입력
Docker Compose를 사용하는 경우라면 아래처럼 환경변수를 주입한다.
# docker-compose.yml
services:
app:
image: my-service:latest
environment:
- SPRING_PROFILES_ACTIVE=dev # 프로필 지정
- DB_PASSWORD=secretpassword # 민감 정보는 환경변수로
local과 prod 두 파일만으로도 충분하다.
별도 dev 서버가 없고, 배포 환경도 단순한 경우에 해당한다.
application.yaml
application-local.yaml
application-prod.yaml
이 정도 규모에서는 설정 파일보다 .gitignore에 민감 정보 파일을 추가하는 것이 더 중요한 관심사다.
dev 서버가 별도로 존재하고, QA(품질 검증) 담당자나 테스터가 있는 경우에 해당한다.
application.yaml
application-local.yaml
application-dev.yaml # 팀원들이 공유하는 개발 서버
application-prod.yaml
application-test.yaml # CI/CD 파이프라인용
이 단계부터는 dev 파일의 민감 정보 관리가 중요해진다.
application-dev.yaml을 Git에 커밋하되, 비밀번호와 키 값은 ${ENV_VAR} 형태로 환경변수 참조를 사용한다.
이 규모부터는 파일 분리만으로는 한계가 있다.
Spring Cloud Config Server 또는 HashiCorp Vault, AWS Secrets Manager 같은 중앙화된 설정 관리 시스템을 도입하는 것이 일반적이다.
# 각 마이크로서비스는 공통 설정을 Config Server에서 가져온다
spring:
config:
import: "configserver:http://config-server:8888"
Config Server(설정 서버) : 여러 서비스의 설정을 Git 저장소 등에서 중앙 관리하고, 각 서비스가 실행 시 설정을 가져오는 구조다. 설정 변경 시 서비스 재배포 없이 반영할 수 있다는 장점이 있다.
application-prod.yaml에 비밀번호, JWT 시크릿, API 키를 직접 써서 Git에 올리면 안 된다.
이것은 보안 사고의 가장 흔한 원인 중 하나다.
# yaml 안에서 환경변수를 참조하는 방식
spring:
datasource:
password: ${DB_PASSWORD} # 환경변수가 없으면 에러 발생
username: ${DB_USERNAME:defaultuser} # 기본값 지정 가능
.gitignore로 민감 파일 보호# 민감 정보가 담긴 파일은 Git 추적에서 제외
application-prod.yaml
application-secret.yaml
*.env
현재 팀 단위 이상의 프로젝트라면 아래 방식들이 표준처럼 자리잡고 있다.
| 방식 | 설명 | 적합한 규모 |
|---|---|---|
| 환경변수 직접 주입 | 가장 간단, Docker/서버에서 설정 | 소규모 |
| AWS Secrets Manager | AWS 인프라 위에서 비밀값 중앙 관리 | 중~대규모 |
| HashiCorp Vault | 클라우드 무관, 강력한 시크릿 관리 | 중~대규모 |
| Kubernetes Secret | k8s(쿠버네티스) 환경에서 마운트 방식으로 주입 | k8s 기반 |
Kubernetes 환경이라면 Secret을 Pod의 환경변수로 주입하는 방식이 일반적이다.
# k8s Deployment에서 Secret을 환경변수로 주입하는 예시
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret # kubectl로 생성한 Secret 이름
key: password # Secret 내의 키
Spring Boot 애플리케이션은 이 환경변수를 ${DB_PASSWORD}로 읽으면 된다.
즉, Spring 코드 자체는 어디서 값이 오는지 알 필요가 없다.
@Value vs @ConfigurationProperties설정 파일을 Java 코드에서 읽을 때 두 가지 방식이 있다.
@Value — 간단하지만 규모에 취약하다@Service
public class JwtService {
// 단일 값 주입에는 편리하다
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration:3600}") // 기본값 3600
private long expiration;
}
단점: 설정이 늘어날수록 클래스 여기저기에 @Value가 흩어진다. 오타가 있어도 컴파일 시에는 발견되지 않고 런타임에 에러가 발생한다.
@ConfigurationProperties — 구조화된 방식 (권장)// Spring Boot 3 + Java 17 이상이라면 Record 사용 권장
@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(
String secret,
long expiration, // jwt.expiration
String issuer // jwt.issuer
) {}
# application.yaml
jwt:
secret: ${JWT_SECRET}
expiration: 3600
issuer: my-service
// 활성화 방법 1 : 메인 클래스에 선언
@SpringBootApplication
@EnableConfigurationProperties(JwtProperties.class)
public class MyApplication { ... }
// 또는 @ConfigurationPropertiesScan 으로 자동 스캔도 가능
Spring Boot 3 + Java 17 이상이라면
record와@ConfigurationProperties의 조합이 현재 가장 권장되는 방식이다. 불변성(immutable)이 보장되고, getter/setter 없이도 바인딩이 된다.
정리하면:
| 구분 | @Value | @ConfigurationProperties |
|---|---|---|
| 사용 케이스 | 단일 값 1~2개 | 관련 설정 그룹 3개 이상 |
| 타입 안전성 | 없음 (런타임 에러) | 있음 (컴파일/시작 시 검증) |
| 유지보수 | 코드 분산 | 한 곳에서 관리 |
| 테스트 용이성 | 낮음 | 높음 |
현실적인 이야기를 하자면, 많은 기업 내부 시스템은 아직도 application.properties 단일 파일에 모든 설정이 담겨 있고, 배포 시 담당자가 수동으로 값을 바꾸는 방식을 유지하고 있다. 이런 레거시(오래된 기존 시스템) 환경에서는 프로필 분리를 한 번에 적용하기 어려울 수 있다.
이 경우, 아래처럼 단계적으로 접근하는 것이 현실적이다.
local과 prod만 분리SPRING_PROFILES_ACTIVE 환경변수 주입 추가Spring Boot의 Profile 기반 설정 분리는 단순히 "파일을 나누는 것"이 아니다.
환경별 실수를 방지하고, 팀원 간의 설정 충돌을 줄이며, 배포 프로세스를 안전하게 만드는 기반 아키텍처다.
application.yaml에, 환경별 오버라이드는 application-{profile}.yaml에@ConfigurationProperties + Java Record 조합을 적극 활용한다