개발 환경에서는 H2, 운영에서는 PostgreSQL — 외부 설정과 프로파일 관리

같은 JAR을 개발과 운영에 그대로 배포해야 한다. 개발에서는 H2 인메모리 DB를, 운영에서는 PostgreSQL을 써야 한다. 코드를 고치지 않고 — 설정만 바꿔서. Spring Boot의 외부 설정(externalized configuration)과 프로파일(profile)이 이것을 가능하게 한다.

이 글은 프로퍼티 소스 계층, @ConfigurationProperties, 멀티 프로파일, 환경 변수 매핑, 그리고 비밀 관리(secrets management) 패턴을 풀어간다. (Spring Boot Reference - External Configuration)

프로퍼티 소스 계층 — 17개의 우선순위

Spring Boot는 여러 소스에서 프로퍼티를 읽고, 우선순위가 높은 것이 낮은 것을 덮어쓴다:

flowchart TD
    P1["1. 명령행 인자<br/>--server.port=9090"] --> P2["2. 환경 변수<br/>SERVER_PORT=9090"]
    P2 --> P3["3. SPRING_APPLICATION_JSON"]
    P3 --> P4["4. 프로파일별 yml<br/>application-prod.yml"]
    P4 --> P5["5. 기본 application.yml"]
    P5 --> P6["6. @PropertySource"]
    P6 --> DEFAULT["7. 기본값<br/>(Spring Boot 자동 구성)"]
우선순위 소스 사용 시기
1 명령행 인자 (--key=value) 테스트, 일시적 오버라이드
2 환경 변수 (KEY=value) 프로덕션 배포 (Docker, K8s)
3 SPRING_APPLICATION_JSON 복잡한 JSON 설정
4 프로파일별 yml (application-{profile}.yml) 환경별 설정
5 기본 application.yml 공통 설정
6 @PropertySource 커스텀 프로퍼티 파일

프로덕션 배포에서는 환경 변수를 기본으로 한다 — 12-factor app 원칙. Docker, Kubernetes가 환경 변수로 설정을 주입한다. 코드에 환경별 분기를 넣지 않는다.

왜 환경 변수가 yml보다 우선인가? 같은 JAR 빌드 결과물을 dev/staging/prod에 수정 없이 배포하려면 — 환경마다 달라지는 값(DB 비밀번호, API 키)이 코드에 들어 있으면 안 된다. 환경 변수는 OS 컨테이너 수준에서 주입되므로, JAR 빌드와 설정 분리가 완벽히 된다. application-prod.ymlpassword: ${DB_PASSWORD}를 쓰면 — yml은 "이 자리에 환경 변수 값을 넣어라"라는 자리표시자(placeholder)만 가지고, 실제 값은 배포 시점에 환경에서 온다.

비유: 같은 레시피(빌드된 JAR)로 요리하지만 — 각 식당(환경)마다 "오늘의 재료"(환경 변수)를 다르게 공급받는다. 레시피를 고칠 필요 없이, 식당마다 재료만 바꾼다.

내부 메커니즘: 환경 변수 SPRING_DATASOURCE_URL이 어떻게 spring.datasource.url 프로퍼티로 매핑되는가? Spring Boot의 Relaxed Binding 규칙이 변환한다: (1) 밑줄(_)을 점(.)으로, (2) 대문자를 소문자로. SPRING_DATASOURCE_URLspring.datasource.url. 이 규칙 덕분에 환경 변수 이름이 Spring 프로퍼티 키로 자동 매핑된다. 반대 방향으로는 spring.datasource.url 프로퍼티를 환경 변수로 설정할 때 SPRING_DATASOURCE_URL로 적으면 된다.

환경 변수 매핑 규칙

spring.datasource.url  →  SPRING_DATASOURCE_URL
spring.datasource.password  →  SPRING_DATASOURCE_PASSWORD
server.port  →  SERVER_PORT

점(.)은 밑줄(_)로, 소문자는 대문자로 변환된다. 이 규칙으로 환경 변수만으로 모든 프로퍼티를 오버라이드할 수 있다.

@ConfigurationProperties — 타입 안전 바인딩

// Spring Boot 4 / Java 25 — @ConfigurationProperties
@ConfigurationProperties(prefix = "app")
public record AppProperties(
    String name,
    int maxConnections,
    DatabaseConfig database,
    List<String> allowedOrigins,
    Map<String, String> features
) {
    public record DatabaseConfig(String url, String username, String password) {}
}

// application.yml
# app:
#   name: my-service
#   max-connections: 100
#   database:
#     url: jdbc:postgresql://db:5432/myapp
#     username: appuser
#     password: secret
#   allowed-origins:
#     - https://app.example.com
#     - https://admin.example.com
#   features:
#     audit: enabled
#     caching: disabled

@ConfigurationProperties record는 프로퍼티를 타입 안전하게 바인딩한다. @Value보다 장점: 컴파일 타임 검증, IDE 자동 완성, 중첩 구조, List/Map 지원, record 불변성.

@Validated로 프로퍼티 검증

// Spring Boot 4 / Java 25 — 프로퍼티 검증
@ConfigurationProperties(prefix = "app")
@Validated   // 프로퍼티 바인딩 시 검증
public record AppProperties(
    @NotBlank String name,
    @Min(1) @Max(10000) int maxConnections,
    @Valid @NotNull DatabaseConfig database
) {
    public record DatabaseConfig(
        @NotBlank String url,
        @NotBlank String username
    ) {}
}
// app.name이 비어 있으면 애플리케이션 시작 실패

멀티 프로파일 — 한 파일에서 환경 분리

# Spring Boot 4 / Java 25 — application.yml (다중 프로파일)
spring:
  application:
    name: my-service
  profiles:
    active: dev   # 기본 프로파일

# 공통 설정
server:
  port: 8080

logging:
  level:
    root: INFO

---
# 개발 환경
spring:
  config:
    activate:
      on-profile: dev

spring:
  datasource:
    url: jdbc:h2:mem:devdb
    username: sa
    password:

logging:
  level:
    com.example: DEBUG

---
# 운영 환경
spring:
  config:
    activate:
      on-profile: prod

spring:
  datasource:
    url: ${DB_URL:jdbc:postgresql://prod-db:5432/myapp}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

logging:
  level:
    com.example: WARN
    root: WARN

---로 YAML 문서를 분리한다. spring.config.activate.on-profile로 각 섹션의 활성 프로파일을 지정. 프로파일 전환: --spring.profiles.active=prod 또는 SPRING_PROFILES_ACTIVE=prod.

프로파일별 파일 분리

src/main/resources/
├── application.yml          # 공통
├── application-dev.yml      # 개발
├── application-staging.yml  # 스테이징
└── application-prod.yml     # 운영

한 파일에 다중 프로파일을 두거나, 프로파일별 파일로 분리하거나 — 팀 선호에 따라 선택. 파일이 크면 분리가 더 관리하기 쉽다.

비밀 관리(Secrets Management) — 비밀번호를 yml에 넣지 마라

# 나쁜 예: 비밀번호가 코드에 하드코딩
spring:
  datasource:
    password: super-secret-123   # 절대 하지 마라

# 좋은 예: 환경 변수로 주입
spring:
  datasource:
    password: ${DB_PASSWORD}   # 환경 변수에서 읽음

비밀 관리 패턴

방식 용도
환경 변수 단일 인스턴스 DB_PASSWORD=secret
Vault / AWS Secrets Manager 마이크로서비스 Spring Cloud Vault
Kubernetes Secrets K8s 환경 secretKeyRef
${VAR:default} 선택적 기본값 ${DB_PASSWORD:devpass}

비밀번호, API 키, 인증서는 절대 yml 파일에 평문으로 넣지 않는다. 환경 변수, Vault, K8s Secrets로 관리한다. Git에 비밀이 커밋되면 보안 사고다.

Spring Boot 4.0 — 프로퍼티 마이그레이션

Spring Boot 버전이 올라가면서 프로퍼티 이름이 변경될 수 있다. Spring Boot 4.0은 properties migrator를 제공한다:

<!-- 마이그레이션 중 임시 의존성 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-properties-migrator</artifactId>
    <scope>runtime</scope>
</dependency>

properties migrator는 시작 시 이전 프로퍼티 이름을 감지하고, 새 이름으로 자동 마이그레이션하며 경고를 출력한다. 마이그레이션 완료 후 이 의존성을 제거한다. (Spring Boot Reference - Properties Migrator)

실습 — 프로퍼티 계층 시뮬레이션

// Spring Boot 4 / Java 25 — PropertyHierarchyDemo.java
import java.util.*;

public class PropertyHierarchyDemo {
    public static void main(String[] args) {
        // 우선순위 시뮬레이션 (낮은 것이 나중에 덮어씀)
        Map<String, String> config = new HashMap<>();

        // 5. 기본 yml
        config.put("server.port", "8080");
        config.put("db.url", "jdbc:h2:mem:dev");
        config.put("log.level", "DEBUG");

        // 4. 프로파일 yml (prod)
        config.put("db.url", "jdbc:postgresql://prod-db:5432/myapp");
        config.put("log.level", "WARN");

        // 2. 환경 변수
        config.put("server.port", "9090");

        // 1. 명령행 인자
        config.put("log.level", "ERROR");

        System.out.println("최종 설정:");
        config.forEach((k, v) -> System.out.println("  " + k + " = " + v));
    }
}
java PropertyHierarchyDemo.java
최종 설정:
  server.port = 9090
  db.url = jdbc:postgresql://prod-db:5432/myapp
  log.level = ERROR

확인할 것: 각 소스가 우선순위에 따라 덮어쓴다. 최종 값은 가장 우선순위가 높은 소스에서 결정된다.

Spring Cloud Config — 중앙 집중식 프로퍼티 관리

마이크로서비스 환경에서는 각 서비스가 자체 application.yml을 가지면 설정이 분산된다. Spring Cloud Config로 중앙 저장소(Git, Vault)에서 모든 서비스의 설정을 관리한다:

# Spring Cloud Config Client (각 마이크로서비스)
spring:
  config:
    import: optional:configserver:http://config-server:8888
  cloud:
    config:
      name: my-service
      profile: prod

Spring Cloud Config Server는 Git 저장소에서 프로퍼티를 읽어 각 서비스에 배포한다. 설정 변경 시 서비스 재배포 없이 /actuator/refresh로 런타임 갱신할 수 있다. (Spring Cloud 생태계 — 본 학습서 범위 밖)

요약 — 이 글의 결론

  • 프로퍼티 소스 계층으로 환경별 설정을 외부화한다. 명령행 > 환경변수 > 프로파일 yml > 기본 yml. 프로덕션은 환경 변수를 기본으로.
  • @ConfigurationProperties record로 타입 안전 바인딩을 한다. @Validated로 시작 시 검증. @Value보다 안전하고 IDE 친화적.
  • 멀티 프로파일로 환경을 분리한다. ---로 한 파일에 다중 프로파일 또는 파일 분리. spring.profiles.active로 전환.
  • 비밀번호를 yml에 하드코딩하지 않는다. 환경 변수, Vault, K8s Secrets로 관리. ${VAR:default}로 환경 변수 주입.
  • Spring Boot 4.0의 properties migrator로 버전 간 프로퍼티 변경을 안전하게 마이그레이션한다.

생각해 볼 문제

  1. 환경 변수 SPRING_DATASOURCE_URLapplication.ymlspring.datasource.url을 덮어쓰는 규칙(relaxed binding)을 설명하라.
  2. @ConfigurationProperties record에 @DefaultValue("42")를 붙이면 어떻게 동작하는가?
  3. 여러 프로파일(dev, fast, cloud)을 동시에 활성화할 때 우선순위는?
  4. Spring Cloud Config Server로 중앙 집중식 프로퍼티 관리를 할 때의 장단점은?
  5. ${DB_PASSWORD:#{environment['DB_PASSWORD']}}와 같은 SpEL 기반 프로퍼티 참조는 언제 필요한가?
  6. Spring Boot 4.0의 properties migrator가 마이그레이션을 수행하는 방식은? (renamed vs removed 프로퍼티)

참고

  1. Spring Boot 4.0과 이전 버전(Spring Boot 3.x)의 가장 큰 차이점은 무엇인가? 마이그레이션 시 주의할 점은?
  2. 이 글에서 다룬 개념을 실제 프로젝트에 적용할 때 가장 먼저 고려해야 할 것은?

'Develop Artifacts > Spring Boot' 카테고리의 다른 글

SpringBoot - 10. deployment  (0) 2026.07.12
SpringBoot - 08. actuator  (0) 2026.07.12
SpringBoot - 07. security  (0) 2026.07.12
SpringBoot - 06. testing  (0) 2026.07.12
SpringBoot - 05. spring-data-jpa  (0) 2026.07.12