카테고리 없음

Spring Boot 프로파일(profile)로 dev/stage/prod 환경 분리하기 (설정 오버라이드 전략까지)

IT Lab 2026. 3. 6. 20:00

Spring Boot 3에서 profile로 dev/stage/prod 설정을 분리하고, spring.profiles.active와 설정 오버라이드 우선순위를 실무 관점에서 정리합니다.

도입 (문제 상황)

로컬에서는 잘 되는데 스테이징에서만 DB가 안 붙거나, 운영에 디버그 로그가 켜져 있는 걸 뒤늦게 발견한 적 있으실 거예요. 환경마다 설정이 다른 건 자연스러운 일인데, 그 차이를 “안전하게” 관리하는 게 생각보다 어렵습니다. 이럴 때 Spring Boot의 프로파일(profile)이 가장 간단하고 강력한 분리 도구가 됩니다.

핵심 개념: Spring Boot 프로파일과 설정 오버라이드가 중요한 이유

Spring Boot에서 프로파일은 “환경별로 다른 설정 묶음”을 선택하는 스위치입니다. dev/stage/prod처럼 실행 환경이 바뀔 때 같은 코드로 동작하되, 설정만 다르게 가져가도록 해줍니다. 비유하자면, 앱은 같은데 “리모컨(프로파일)”로 채널(설정)을 바꾸는 느낌이에요.

특히 실무에서 프로파일이 중요한 이유는 두 가지입니다.

  1. 사고를 줄이는 안전장치
    운영(prod)에서 H2를 물리거나, dev용 테스트 키/토큰이 남아 있거나, 과도한 로그가 켜져 있으면 장애/보안 이슈로 바로 이어질 수 있습니다. 프로파일 분리는 이런 실수를 구조적으로 줄여줍니다.
  2. 오버라이드(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"]

설정 소스들이 우선순위에 따라 합쳐져 최종 프로퍼티가 결정됩니다.

Spring Boot 프로파일별 설정 로딩과 오버라이드 흐름

코드 예제: 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.activeapplication.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 빠르게 세팅