Spring Boot 3에서 profile로 dev/stage/prod 설정을 분리하고, spring.profiles.active와 설정 오버라이드 우선순위를 실무 관점에서 정리합니다.
도입 (문제 상황)
로컬에서는 잘 되는데 스테이징에서만 DB가 안 붙거나, 운영에 디버그 로그가 켜져 있는 걸 뒤늦게 발견한 적 있으실 거예요. 환경마다 설정이 다른 건 자연스러운 일인데, 그 차이를 “안전하게” 관리하는 게 생각보다 어렵습니다. 이럴 때 Spring Boot의 프로파일(profile)이 가장 간단하고 강력한 분리 도구가 됩니다.
핵심 개념: Spring Boot 프로파일과 설정 오버라이드가 중요한 이유
Spring Boot에서 프로파일은 “환경별로 다른 설정 묶음”을 선택하는 스위치입니다. dev/stage/prod처럼 실행 환경이 바뀔 때 같은 코드로 동작하되, 설정만 다르게 가져가도록 해줍니다. 비유하자면, 앱은 같은데 “리모컨(프로파일)”로 채널(설정)을 바꾸는 느낌이에요.
특히 실무에서 프로파일이 중요한 이유는 두 가지입니다.
- 사고를 줄이는 안전장치
운영(prod)에서 H2를 물리거나, dev용 테스트 키/토큰이 남아 있거나, 과도한 로그가 켜져 있으면 장애/보안 이슈로 바로 이어질 수 있습니다. 프로파일 분리는 이런 실수를 구조적으로 줄여줍니다. - 오버라이드(override) 전략이 곧 운영 전략
Spring Boot 설정은 “한 군데”에서만 읽지 않습니다.application.yml,application-prod.yml, 환경 변수, 커맨드라인 인자 등 여러 소스가 겹치며, 결국 우선순위에 따라 최종 값이 결정됩니다.
즉 “어디에 어떤 값을 두고, 무엇이 무엇을 덮어쓰게 할지”를 정해두는 게 곧 팀의 배포/운영 방식이 됩니다.
아래 표는 프로파일을 쓸 때 가장 자주 쓰는 파일/방식을 한눈에 정리한 것입니다.
| 목적 | 추천 방식 | 예시 | 비고 |
|---|---|---|---|
| 공통 기본값 | application.yml |
서버 포트, 공통 로그 포맷 | 모든 환경에 적용 |
| 환경별 값 | application-{profile}.yml |
DB URL, 외부 API 엔드포인트 | profile에 따라 자동 로드 |
| 배포 시점 주입(비밀/가변) | 환경 변수 / Secret | SPRING_DATASOURCE_PASSWORD |
Git에 두지 않기 |
| 임시 테스트 | 커맨드라인 인자 | --spring.profiles.active=dev |
가장 강하게 덮어씀 |
프로파일 선택은 보통 spring.profiles.active로 합니다. 설정 위치는 여러 곳이 될 수 있지만, 실무에서는 배포 파이프라인에서 환경 변수로 주입하는 패턴이 가장 흔합니다.
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
그리고 “오버라이드 우선순위”는 항상 염두에 두셔야 합니다. 모든 우선순위를 외우실 필요는 없지만, 최소한 이 감각은 중요합니다.
- 커맨드라인 인자/환경 변수는 강력하게 덮어쓰는 편
application.yml은 기본값application-prod.yml같은 프로파일 파일은 “해당 프로파일일 때 기본값을 덮어씀”
아래 다이어그램처럼 “여러 설정 소스가 합쳐져 최종 설정이 된다”는 흐름을 이해하면 디버깅이 쉬워집니다.
flowchart TD
A["application.yml (default)"] --> D["Final Properties"]
B["application-{profile}.yml"] --> D["Final Properties"]
C["Env Vars / Command Line"] --> D["Final Properties"]
설정 소스들이 우선순위에 따라 합쳐져 최종 프로퍼티가 결정됩니다.

코드 예제: dev/stage/prod 프로파일 분리 + spring.profiles.active 적용
아래 예제는 Spring Boot 3.x(Java 17) 기준이며, 그대로 복붙해서 실행할 수 있는 형태로 구성했습니다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// 설정 확인용 actuator (실무에서도 자주 씁니다)
implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml (공통)
spring:
application:
name: profile-demo
server:
port: 8080
management:
endpoints:
web:
exposure:
include: "health,info,env" # 예제용. 운영에서는 env 노출 주의!
application-dev.yml (개발)
app:
message: "Hello from dev"
logging:
level:
root: INFO
com.example: DEBUG
application-stage.yml (스테이징)
app:
message: "Hello from stage"
logging:
level:
root: INFO
com.example: INFO
application-prod.yml (운영)
app:
message: "Hello from prod"
logging:
level:
root: WARN
com.example: INFO
간단한 컨트롤러/설정 바인딩
package com.example.profiledemo;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app")
public record AppProperties(String message) {
}
package com.example.profiledemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
public class ProfileDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ProfileDemoApplication.class, args);
}
}
package com.example.profiledemo;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
@RestController
public class HelloController {
private final AppProperties props;
private final Environment env;
public HelloController(AppProperties props, Environment env) {
this.props = props;
this.env = env;
}
@GetMapping("/hello")
public String hello() {
return props.message() + " / activeProfiles=" + Arrays.toString(env.getActiveProfiles());
}
}
실행 방법
- dev로 실행
./gradlew bootRun --args='--spring.profiles.active=dev'
- stage로 실행
java -jar build/libs/0.0.1-SNAPSHOT.jar --spring.profiles.active=stage
- prod로 실행(환경 변수 권장)
SPRING_PROFILES_ACTIVE=prod java -jar build/libs/0.0.1-SNAPSHOT.jar
이제 GET http://localhost:8080/hello 호출 시, 선택한 프로파일에 따라 메시지와 로깅 레벨이 달라집니다.
참고:
spring.profiles.active를application.yml에 박아두는 방식은 로컬 개발에서는 편해 보이지만, 배포 환경에서 “기본값이 dev로 고정되는” 사고로 이어질 수 있어 조심하시는 편이 좋습니다.
실무 팁
💡 실무에서는: 운영(prod) 프로파일을 “기본값”으로 두지 말고, 배포 파이프라인에서 명시적으로 주입하세요
운영에서 가장 위험한 건 “의도치 않은 기본값”입니다.SPRING_PROFILES_ACTIVE를 Kubernetes Deployment/Helm values, ECS Task Definition, Jenkins/GitHub Actions 변수 등 배포 정의에 넣고, 애플리케이션은 프로파일을 스스로 추측하지 않게 두는 게 안전합니다.
💡 실무에서는: 비밀값은 프로파일 파일이 아니라 Secret/환경 변수로 오버라이드하세요
application-prod.yml에 DB 비밀번호나 API 키를 넣으면 Git 히스토리에 남습니다. 대신application-prod.yml에는 “키 이름(placeholder)”만 두고, 실제 값은 Secret Manager/Vault/K8s Secret에서 환경 변수로 주입해 덮어쓰는 구성이 현실적으로 가장 많이 씁니다.
(예:SPRING_DATASOURCE_PASSWORD,APP_EXTERNAL_API_KEY등)
핵심 요약: 프로파일은 같은 코드로 dev/stage/prod 설정을 안전하게 분리하는 스위치입니다.
핵심 요약: 설정은 여러 소스가 합쳐지며, 오버라이드 우선순위가 운영 품질을 좌우합니다.
핵심 요약: 운영 프로파일/비밀값은 배포 시점에 “명시적으로 주입”하는 패턴이 가장 안전합니다.
다음 글: #06 로깅 기본: SLF4J + Logback 빠르게 세팅