Spring Boot

Spring Boot application.yml 설정 기본기 — properties vs yml, 계층 구조, 타입 바인딩까지

IT Lab 2026. 3. 6. 10:00

Spring Boot 3.x에서 application.yml을 제대로 쓰기 위한 기본기: properties와의 차이, 계층 구조 설계, @ConfigurationProperties 타입 바인딩, 설정 키 네이밍 팁을 실무 관점으로 정리합니다.

1) 도입 (문제 상황)

Spring Boot 프로젝트를 시작하면 가장 먼저 만지는 파일이 application.yml인데, 막상 설정이 늘어나면 “이 키를 어디에 둬야 하지?”, “점(.)으로 쓰는 게 맞나, 하이픈(-)이 맞나?” 같은 고민이 생기기 쉽습니다.
@Value로 여기저기 주입하다가 타입 변환이나 기본값 처리에서 한 번쯤은 삐끗해 보셨을 거예요.

2) 핵심 개념 (Spring Boot application.yml 설정을 잘 쓰는 이유)

Spring Boot 설정은 “외부화(Externalized Configuration)”가 핵심이에요

Spring Boot는 코드와 설정을 분리해 환경별로 다르게 동작하게 만들기 쉽게 설계되어 있습니다. 운영/스테이징/로컬에서 DB 주소나 외부 API 엔드포인트가 달라지는 건 자연스러운 일이니까요.
그래서 application.yml을 “그냥 키-값 파일”로 보지 말고, 도메인별 설정을 구조화하고, 타입으로 안전하게 바인딩하는 게 중요합니다.

properties vs yml: 무엇이 더 좋나요?

둘 다 지원되며, 기능적으로 “불가능/가능” 차이는 거의 없습니다. 다만 팀이 커지고 설정이 많아질수록 가독성과 구조화에서 차이가 납니다.

항목 application.properties application.yml
표현 방식 a.b.c=value 들여쓰기 기반 계층 구조
가독성/구조화 설정이 커지면 한 줄이 길어짐 묶음이 자연스럽고 읽기 쉬움
실수 포인트 키 오타(점/하이픈), 중복 키 들여쓰기(스페이스) 실수, 탭 사용
추천 상황 설정이 아주 단순하거나 기존 레거시 대부분의 Spring Boot 서비스

정리하면, 설정이 조금이라도 커질 가능성이 있다면 yml이 유지보수에 유리한 편입니다.

계층 구조: “기능 단위로 묶고, 과도한 중첩은 피하기”

YAML의 강점은 계층 구조입니다. 예를 들어 “외부 API 호출” 설정이라면 아래처럼 한 덩어리로 묶어두면 찾기 쉽습니다.

  • app.payment.* : 결제 도메인 설정
  • app.notification.* : 알림 도메인 설정
  • external.kakao.* : 특정 벤더(외부) 설정

중요한 포인트는 너무 깊게 파지 않는 것입니다. 4~5단계 이상 중첩되면 오히려 키를 찾기 어렵고, 바인딩 클래스도 복잡해집니다. “서랍장(최상위 prefix) → 칸(도메인) → 소품(세부 옵션)” 정도로 생각하면 적당합니다.

타입 바인딩 맛보기: @Value보다 @ConfigurationProperties

설정이 한두 개면 @Value("${...}")도 괜찮지만, 설정이 묶음으로 늘어나면 다음 문제가 생깁니다.

  • 타입 변환/기본값/검증이 흩어짐
  • 키 이름이 코드 곳곳에 문자열로 박힘
  • 설정 구조 변경 시 수정 범위가 커짐

이럴 때 @ConfigurationProperties는 설정을 하나의 타입(클래스)로 묶어 주입할 수 있어요. “설정도 DTO처럼 다룬다”라고 생각하시면 이해가 빠릅니다.

설정 키 네이밍 팁: kebab-case + 명확한 prefix

Spring Boot의 “relaxed binding” 덕분에 my-app.timeout-ms, myApp.timeoutMs, MY_APP_TIMEOUT_MS 같은 형태가 어느 정도 매핑됩니다.
그렇더라도 실무에서는 파일에는 kebab-case(하이픈)로 통일하는 편이 충돌이 적고 읽기 좋습니다.

  • 파일(YAML/Properties): read-timeout, base-url, max-retries
  • 자바 필드: readTimeout, baseUrl, maxRetries

prefix는 app.*처럼 너무 포괄적으로 시작해도 되지만, 팀/서비스가 커지면 app 아래에 도메인 단위로 명확히 나누는 걸 추천합니다.

flowchart TD
  A["application.yml"] --> B["Spring Boot Binder"]
  B --> C["@ConfigurationProperties Class"]
  C --> D["Service/Component"]

설정 파일 값이 Binder를 통해 타입 안전한 설정 클래스로 바인딩된 뒤, 서비스로 주입되는 흐름입니다.

application.yml이 @ConfigurationProperties로 바인딩되는 흐름

3) 코드 예제 (Spring Boot 3.x에서 application.yml + 타입 바인딩 실행 예)

아래 예제는 application.yml에 있는 설정을 @ConfigurationProperties로 바인딩하고, 시작 시 로그로 출력하는 최소 구성입니다. 그대로 복붙해서 실행 가능합니다. (Java 17)

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.2'
    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'
    // @ConfigurationProperties 메타데이터(IDE 자동완성)에 도움
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

application.yml

app:
  external-api:
    base-url: "https://api.example.com"
    connect-timeout: 1s
    read-timeout: 2s
    max-retries: 3
    headers:
      x-client-id: "my-client"
      x-trace-enabled: "true"

메인 클래스

package com.example.demo;

import com.example.demo.config.ExternalApiProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan // @ConfigurationProperties 클래스를 스캔해서 빈으로 등록
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

설정 바인딩 클래스 (@ConfigurationProperties)

package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;
import java.util.Map;

@ConfigurationProperties(prefix = "app.external-api")
public record ExternalApiProperties(
        String baseUrl,
        Duration connectTimeout,
        Duration readTimeout,
        int maxRetries,
        Map<String, String> headers
) {
}
  • Duration1s, 200ms 같은 형태로 자연스럽게 바인딩됩니다.
  • YAML의 base-url → Java의 baseUrl로 매핑됩니다(케밥/카멜 변환).

설정을 사용하는 컴포넌트 (시작 시 출력)

package com.example.demo;

import com.example.demo.config.ExternalApiProperties;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PrintConfig {

    @Bean
    ApplicationRunner print(ExternalApiProperties props) {
        return args -> {
            System.out.println("baseUrl=" + props.baseUrl());
            System.out.println("connectTimeout=" + props.connectTimeout());
            System.out.println("readTimeout=" + props.readTimeout());
            System.out.println("maxRetries=" + props.maxRetries());
            System.out.println("headers=" + props.headers());
        };
    }
}

실행하면 콘솔에 설정 값이 출력됩니다. 이 방식의 장점은 설정이 늘어나도 ExternalApiProperties만 보면 “이 기능이 어떤 설정을 갖는지”가 한눈에 들어온다는 점입니다.

참고: @Value는 여전히 유용하지만, 여러 키를 묶어서 관리해야 할 때는 @ConfigurationProperties가 유지보수 난이도를 확 낮춰줍니다.

4) 실무 팁

💡 실무에서는: YAML 들여쓰기/탭 실수로 장애가 나기도 해요

  • YAML은 탭을 허용하지 않고 스페이스 들여쓰기에 민감합니다.
  • CI에서 yamllint 같은 린터를 강제하거나, 최소한 IDE에서 “탭 → 스페이스” 변환을 켜두는 걸 추천합니다.
  • 설정이 커질수록 “사람이 눈으로 검증”하기 어렵기 때문에, 포맷/검증 자동화가 효과가 큽니다.

💡 실무에서는: 설정 키는 “변경 비용”이 큰 API처럼 다루는 게 좋아요

  • 설정 키를 한 번 배포하면 운영 문서, 배포 스크립트, Helm values, Secret/ConfigMap 등 여러 곳에 퍼집니다.
  • 그래서 키 네이밍은 초기에 prefix(예: app.external-api)를 명확히 잡고, 중간에 바꾸지 않는 게 좋습니다.
  • 꼭 바꿔야 한다면 구키/신키를 일정 기간 공존시키는 전략(마이그레이션)을 먼저 설계해 보세요.

핵심 요약

  • application.yml은 설정을 “구조화”하기 좋아서 규모가 커질수록 유리합니다.
  • 여러 설정을 묶어 쓸 때는 @ConfigurationProperties로 타입 안전하게 바인딩하는 게 유지보수에 좋습니다.
  • 키 네이밍은 YAML에서는 kebab-case로 통일하고, prefix를 명확히 잡아 변경 비용을 줄이세요.

다음 글: [#05 프로파일(profile)로 환경 분리하기]