@Value를 남용할 때 생기는 유지보수/검증 문제를 정리하고, Spring Boot 3의 @ConfigurationProperties + Validation로 설정을 타입 안전하게 분리하는 방법을 예제로 보여드립니다.
도입 (문제 상황)
@Value("${...}")를 여기저기 붙여서 설정을 읽다 보면, 어느 순간 “이 설정이 어디서 쓰이지?”를 추적하기가 꽤 힘들어집니다. 게다가 숫자/URL/시간 같은 값이 문자열로 흘러다니다가 런타임에야 터지는 경우도 자주 보게 됩니다. 설정을 타입 안전하게 받고, 검증까지 한 번에 묶어두는 방법이 필요합니다.
핵심 개념 — Spring Boot @ConfigurationProperties가 중요한 이유
Spring Boot에서 설정을 다루는 방식은 크게 두 가지로 나뉩니다.
@Value: “한 값”을 빠르게 주입하기는 편하지만, 값이 늘어나면 산재하고, 타입 변환/기본값/검증이 흩어지기 쉽습니다.@ConfigurationProperties: 특정 prefix 아래 설정을 한 클래스에 응집하고, 필드를 타입으로 선언하며, Bean Validation으로 시작 시점에 검증까지 연결할 수 있습니다.
설정을 @ConfigurationProperties로 모아두면, 마치 “설정 전용 DTO”처럼 동작합니다. 코드 곳곳에 흩어진 @Value는 여러 사람이 동시에 메모를 붙여놓은 벽면 같고, @ConfigurationProperties는 문서화된 설정 파일 한 권에 정리해 둔 느낌에 가깝습니다.
@Value vs @ConfigurationProperties 비교
| 항목 | @Value | @ConfigurationProperties |
|---|---|---|
| 설정 위치 | 코드 전반에 분산되기 쉬움 | prefix 기준으로 한 클래스에 응집 |
| 타입 안전성 | 제한적(SpEL/문자열 의존) | 강함(필드 타입 기반 바인딩) |
| 검증(Validation) | 직접 if 체크 필요 | @Validated + Bean Validation로 자동 |
| 유지보수 | 설정 늘수록 추적 어려움 | 설정 구조가 명확하고 확장 쉬움 |
| 추천 용도 | 정말 단일 값 1~2개 | 기능 단위 설정 묶음(대부분의 경우) |
설정 바인딩과 검증 흐름(부팅 시점)

flowchart LR
A["application.yml"] --> B["Binder"]
B --> C["@ConfigurationProperties Bean"]
C --> D["Bean Validation"]
D --> E["ApplicationContext Ready"]
부팅 시 설정이 바인딩되고 검증에 실패하면, 애플리케이션이 “조용히 잘못된 값으로 실행”되지 않고 바로 실패합니다. 운영에서 가장 무서운 건 “실패”가 아니라 “조용히 잘못 동작”이기 때문에, 이 차이는 생각보다 큽니다.
위 다이어그램은 Spring Boot가 설정을 바인딩한 뒤 검증을 거쳐 컨텍스트를 준비하는 흐름을 보여줍니다.
코드 예제 — @ConfigurationProperties + Validation로 설정 클래스 분리하기
아래 코드는 Spring Boot 3.x / Java 17 기준으로 그대로 복붙해서 실행할 수 있는 형태입니다. 예시는 “외부 API 호출”에 필요한 설정을 app.external prefix로 묶고, URL/타임아웃/재시도 횟수를 검증합니다.
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-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml
app:
external:
base-url: "https://api.example.com"
connect-timeout: 1s
read-timeout: 2s
retry:
max-attempts: 3
설정 클래스: ExternalApiProperties.java
package com.example.demo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.hibernate.validator.constraints.URL;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.time.Duration;
@Validated
@ConfigurationProperties(prefix = "app.external")
public class ExternalApiProperties {
@NotNull
@URL // URL 형식 검증(https://... 같은 형태)
private String baseUrl;
@NotNull
private Duration connectTimeout;
@NotNull
private Duration readTimeout;
@Valid
@NotNull
private Retry retry = new Retry();
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public Duration getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(Duration connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Duration getReadTimeout() {
return readTimeout;
}
public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}
public Retry getRetry() {
return retry;
}
public void setRetry(Retry retry) {
this.retry = retry;
}
public static class Retry {
@Min(0)
@Max(10)
private int maxAttempts = 0;
public int getMaxAttempts() {
return maxAttempts;
}
public void setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
}
}
}
포인트는 3가지입니다.
@ConfigurationProperties(prefix = "app.external")로 설정을 “한 덩어리”로 받습니다.Duration같은 타입을 그대로 쓰면1s,200ms처럼 사람이 읽기 쉬운 값으로 관리할 수 있습니다.@Validated+ 제약 애노테이션으로 부팅 시점 검증이 됩니다(틀리면 즉시 실패).
설정 등록: DemoApplication.java
Spring Boot 3에서는 @ConfigurationPropertiesScan으로 패키지 스캔 등록을 많이 씁니다.
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@ConfigurationPropertiesScan
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
사용 예: ExternalApiClient.java
package com.example.demo;
import org.springframework.stereotype.Component;
@Component
public class ExternalApiClient {
private final ExternalApiProperties props;
public ExternalApiClient(ExternalApiProperties props) {
this.props = props;
}
public String describe() {
return "baseUrl=" + props.getBaseUrl()
+ ", connectTimeout=" + props.getConnectTimeout()
+ ", readTimeout=" + props.getReadTimeout()
+ ", maxAttempts=" + props.getRetry().getMaxAttempts();
}
}
확인용 컨트롤러: DebugController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DebugController {
private final ExternalApiClient client;
public DebugController(ExternalApiClient client) {
this.client = client;
}
@GetMapping("/debug/config")
public String config() {
return client.describe();
}
}
이제 application.yml에서 base-url을 비워두거나(null), connect-timeout을 이상한 문자열로 넣거나, retry.max-attempts를 100으로 올리면 애플리케이션이 부팅 단계에서 명확한 에러로 실패합니다. “나중에 장애로 알게 되는 설정 실수”를 “배포 전에 바로 잡는 실수”로 바꿔주는 셈입니다.
실무 팁
💡 실무에서는: 설정 클래스를 “기능 단위”로 쪼개세요
app.external,app.storage,app.security처럼 prefix를 기능 경계로 잡고, 설정 클래스도 그 경계에 맞춰 분리해 보세요.@Value가 많아지는 순간은 보통 “설정의 소유자가 불명확해지는 순간”이라서, 설정을 모아두는 것만으로도 변경 영향도를 크게 줄일 수 있습니다.
💡 실무에서는: 검증은 “반드시 필요한 것만” 강하게 거세요
모든 필드에 무조건@NotNull을 붙이면 환경별로 유연하게 꺼야 하는 옵션까지 부팅 실패를 만들 수 있습니다. 예를 들어 특정 프로파일에서만 필요한 설정이라면, 프로파일별 yml 분리 또는 별도 설정 클래스로 분리하고, 그 범위 안에서만 검증을 강하게 적용하는 편이 운영에서 더 안전합니다.
핵심 요약: @ConfigurationProperties는 설정을 한곳에 모아 타입 안전하게 관리합니다.@Validated를 붙이면 잘못된 설정을 부팅 시점에 잡아낼 수 있습니다.@Value는 단발성에만 쓰고, 기능 단위 설정은 클래스로 분리해 보세요.
다음 글: #09 Spring MVC 요청 처리 흐름 한눈에 보기