@Value vs @ConfigurationProperties

0. 들어가기 앞서

Spring 에서의 환경 변수 관리 방법이 @Value 를 사용하는 방법만 알고 있었는데, 공부하다 보니 좀 더 체계적으로 관리할 수 있는 방법이 있다는 것을 알게 되어 비교하고 각각의 장단점을 정리해보고자 한다.

0-1. 기본 환경 변수 설정

application.properties
properties
greeting-name=taewoo
greeting-coffee=${greeting-name} is drinking Cafe Cereza
application.yml
yaml
greeting:
  name: taewoo
  coffee: ${greeting.name} is drinking Cafe Cereza

1. @Value 를 사용하는 경우

아래와 같이 POJO 형태의 클래스를 먼저 만들었다.

  • POJO : 특정 기술이나 프레임워크에 종속되지 않은, 순수한 형태의 기본 자바 객체

Coffee.java
java
@Component // 빈 등록 필수
@Getter
public class Coffee {

    @Value("${greeting-name:someone}")
    // @Value("${greeting.name:someone}") → yml 파일의 경우
    // application.properties 의 greeting-name 변수 확인 → 없으면 someone 으로 설정
    private final String name;

    @Value("${greeting-coffee:${greeting-name} is drinking ice coffee}")
    // greeting-coffee 변수 확인 → 없으면 : 뒤의 값으로 설정
    private final String place;

    public Coffee() { // 우선순위 확인을 위한 기본 생성자
        this("기본생성자", "여기 있어요");
    }

    public Coffee(String name, String place) {
        this.name = name;
        this.place = place;
    }
}
테스트 코드
java
@SpringBootTest
class CoffeeServiceTest {

    @Autowired
    private CoffeeService coffeeService;

    @Test
    void getCoffee() {
        System.out.println("커피 주문자 : " + coffeeService.getCoffee().getName());
        System.out.println("카페 장소 : " + coffeeService.getCoffee().getPlace());
    }
}
이렇게 기본 생성자가 정의되어 있어도 호출하는 방식에 따라 결과가 다르게 나온다. (개발자 입장에서 헷갈릴 수 있음)

1-1. Spring 컨테이너에 의해 빈으로 생성되는 경우

CoffeeService.java
java
@Service
@RequiredArgsConstructor
public class CoffeeService {

    private final Coffee coffee;

    public Coffee getCoffee() {
        return coffee;
    }
}
💡
동작 과정
  1. Coffee() <기본생성자> 호출
  2. @Value 가 붙은 필드에 설정파일(properties, yml) 의 값을 주입한다.
  3. 결과적으로 환경변수가 기존 생성자로 초기화된 값을 덮어쓰게 된다.

결과


1-2. 직접 클래스 호출

CoffeeService.java
java
@Service
public class CoffeeService {

    // bean 을 통해 주입되는 것이 아니라 직접 생성 (@Component 가 있으나 마나)
    private Coffee coffee = new Coffee();

    public Coffee getCoffee() {
        return coffee;
    }
}
⚠️
Spring 컨테이너에서 주입받은 (DI) 객체가 아닌 새로 만든 객체이기 때문에 @Value 가 동작하지 않는다.
따라서 일반적인 객체 생성과정과 동일하게 이루어지기 때문에 아래와 같은 결과가 나온다.

결과


1-3. @Value 방식의 단점

❗
  1. 테스트의 어려움
    단위 테스트를 위해 객체 생성 시 Spring 컨텍스트를 반드시 띄워야 한다.
    단순 객체 테스트를 위해 Spring 프레임워크를 의존해야 하여 테스트 속도가 느려진다.
  2. final 키워드와의 충돌
    private final String name; 과 같이 final 로 선언하고 싶어도 @Value 는 Spring 이 객체 생성 후 리플렉션(Reflection API) 을 통해 억지로 집어넣는 방식이다.
    final 은 생성자에서 초기화되어야 하는데 필드 주입은 객체 생성 이후에 발생 → final 사용 불가 또는 런타임 오류 발생 가능
  3. 순환 참조 및 초기화 시점 문제
    @Value 주입 시점은 Bean 생성 완료 이후이다.
    생성자 내부 로직에서 @Value 로 주입받은 변수를 사용하려 하면 주입 전이므로 null 값이 나온다.

하지만 이러한 단점 외에도 간결함 및 빠른 프로토타이핑 (복잡 설정 없이 빠르게 기능 구현하거나, 단순 설정 클래스 만들 때) 생산성이 높다.


1-4. 단점 극복 방안 — @Value 를 이용한 생성자 주입

java
@Component
public class Coffee {
    private final String name;
    private final String place;

    // 생성자 주입
    public Coffee(
        @Value("${greeting-name:someone}") String name,
        @Value("${greeting-coffee:${greeting-name} is drinking ice coffee}") String place
    ) {
        this.name = name;
        this.place = place;
    }
}
✅
효과
  1. 테스트 용이 : new Coffee("이름", "장소"); 로 간단히 단위 테스트 가능
  2. 불변성 보장 : final 필드를 안전하게 사용 가능
  3. 순환 참조 방지 : 객체 생성 시점에 모든 의존성이 주입되므로 초기화 오류 없음
  4. DI 프레임워크 의존성 분리 : Spring 이 아닌 다른 환경에서도 독립적으로 동작 가능

1-5. 생성자 주입의 결과

Value 미동작 일반 객체처럼 사용

java
@Service
public class CoffeeService {

    // 기본생성자 호출 시 컴파일 에러
    private final Coffee coffee = new Coffee("이름", "장소");

    public Coffee getCoffee() { return coffee; }
}

Value 동작 의존성 주입 사용

java
@Service
@RequiredArgsConstructor
public class CoffeeService {

    private final Coffee coffee;

    public Coffee getCoffee() { return coffee; }
}

2. @ConfigurationProperties

사용 의도 : 속성을 정의하고 관련 속성을 그룹화해서, 도구로 검증 가능하고 타입 세이프한 방식으로 속성을 참조하고 사용하기 위함

2-1. 사용해보기

Greeting.java
java
@Getter
@Setter
@ConfigurationProperties(prefix = "greeting")
// greeting.name, greeting.coffee 처럼 application.properties 에 설정 가능
public class Greeting {
    private String name;
    private String coffee;
}
TaepangApplication.java (main)
java
@SpringBootApplication
@ConfigurationPropertiesScan
// @ConfigurationProperties 를 스캔하기 위해 필요 (또는 Greeting 에 @Component 추가)
public class TaepangApplication {
    public static void main(String[] args) {
        SpringApplication.run(TaepangApplication.class, args);
    }
}
CoffeeService.java
java
@Service
@RequiredArgsConstructor
public class CoffeeService {

    private final Greeting greeting; // 의존성 주입

    public Greeting getCoffee() { return greeting; }
}
build.gradle
groovy
implementation 'org.springframework.boot:spring-boot-configuration-processor'
application.properties
properties
greeting.name=newName
greeting.coffee=helloCoffee
// 위 의존성 추가 이후 재빌드 하면 위와 같이 기본 변수 설정이 가능하다.
테스트 코드
java
@SpringBootTest
class CoffeeServiceTest {

    @Autowired
    private Greeting greeting;

    @Test
    void getCoffee() {
        System.out.println("커피 주문자 : " + greeting.getName());
        System.out.println("카페 장소 : " + greeting.getCoffee());
    }
}

테스트 결과


💡
@Value 와의 차이
  1. 설정(환경 변수)을 하나의 POJO 로 정의할 수 있다.
  2. IDE 의 자동완성 가능
  3. 기본값 관리의 명확성 : 코드 내에서 직접 확인 가능
    java
    @ConfigurationProperties(prefix = "coffee")
    public class CoffeeProperties {
        private String name = "someone";       // 자바 자체의 초기값
        private String place = "drinking ice coffee";
    }
    객체 생성 시점에 이미 초기값이 설정되므로, Spring 이 값을 주입하지 않아도 항상 안전하고 유효한 상태이다.
  4. List, Map, 중첩 객체 등 계층형 구조를 깔끔하게 매핑 가능하다.
  5. @Valid 와 함께 사용하여 시작 시 설정값 검증 가능 (오류 발생 시 서버 중단) — 제일 중요
    java
    @ConfigurationProperties(prefix = "coffee")
    @Validated
    public class CoffeeProperties {
        @NotBlank
        private String name;  // 반드시 값이 있어야 함
    
        @Min(1)
        private int price;    // 가격은 최소 1 이상이어야 함
    }

3. @ConfigurationProperties 의 추가 기능 — 서드 파티 옵션

3-1. 서드 파티란?

📖
서드 파티(Third-party)는 '제3자'를 뜻한다. 원천 기술이나 서비스를 제공하는 당사자(퍼스트 파티)와 이를 이용하는 소비자(세컨드 파티)가 아닌, 외부의 다른 개발자나 기업을 의미 (from 나무위키)

Spring 관점의 서드 파티
  • JDBC / ORM
  • Auth0, Okta
  • Lombok, Swagger
  • Redis, Kafka, AWS SDK

Spring 프로젝트에 외부 의존성을 추가하고 해당 의존성(컴포넌트) 문서를 참조해 스프링 빈을 생성할 때 @ConfigurationProperties 가 유용하게 사용된다.


3-2. 예제 컴포넌트 생성해서 적용해보기

Droid.java
java
@Getter
@Setter
public class Droid { // 외부에서 가져온 서드 파티 컴포넌트라고 가정
    // getter, setter 를 통해 Reflection API 로 환경변수가 설정된다.
    private String id, description;
}
SpringConfig.java — 빈 등록 (수동)
java
@Configuration
public class SpringConfig {

    @Bean
    @ConfigurationProperties(prefix = "droid")
    // 외부 서드파티에 직접 붙일 수 없는 경우 사용 (원래는 클래스 위가 권장)
    Droid droid() {
        return new Droid(); // Setter 를 통해 객체의 설정값 주입
    }
}
application.properties
properties
droid.id=BB-8
droid.description=Small, rolling android, Doesn't drink milk.
테스트 코드
java
@SpringBootTest
class CoffeeServiceTest {

    @Autowired
    private Droid droid;

    @Test
    void getCoffee() {
        System.out.println("드로이드 id : " + droid.getId());
        System.out.println("드로이드 설명 : " + droid.getDescription());
    }
}

결과

이런 식으로 @ConfigurationProperties 를 이용하여 서드 파티 컴포넌트의 환경변수를 설정하고 사용 가능하다.


3-3. Setter 없이 생성자로만 빈 등록해서 사용하기 (서드 파티)

Droid.java — record 클래스로 변경
java
// record 클래스 (@Getter, @RequiredArgsConstructor 포함)
public record Droid(String id, String description) {
    // droid.id(); / droid.description(); 처럼 접근 가능
}

// ─── 아래 코드와 동일함 ───

@Getter
public class Droid {
    private final String id, description;

    public Droid(String id, String description) {
        this.id = id;
        this.description = description;
    }
}

3-3-1. @Value 사용

java
@Configuration
public class SpringConfig {

    @Value("${droid.id:defaultId}")
    private String id;

    @Value("${droid.description:defaultDesc}")
    private String description;

    @Bean
    @ConfigurationProperties(prefix = "droid")
    Droid droid() {
        return new Droid(id, description);
    }
}
💡
환경 변수를 가져와서 생성자를 통해 Bean 등록 (필요 변수)

결과

3-3-2. @ConfigurationProperties 사용

Droid.java 수정
java
@ConfigurationProperties(prefix = "droid")
public record Droid(String id, String description) { }

해당 클래스를 환경변수에 이용한다는 @ConfigurationProperties 설정

SpringConfig.java
java
@Configuration
// 방법 1. Droid 를 빈 등록 (이것만 쓰는 경우 Setter 필수 / @ConfigurationPropertiesScan 없어도 됨)
@EnableConfigurationProperties(Droid.class)
public class SpringConfig {

    // 방법 2. 직접 빈 등록 (Setter 없는 경우 보통 이 방법 사용 / @ConfigurationPropertiesScan 필수)
    @Bean
    Droid droid(Droid dro) {
        return new Droid(dro.id(), dro.description());
    }
}

방법 한 개만 써야 중복 빈 등록이 방지된다. (상황에 맞게 사용)

결과


📌
정리
  • Setter 가 있으면 : @ConfigurationPropertiesScan 없어도 됨. 여러 방법 존재 (위 SpringConfig.java 참고)
  • 생성자를 통해 초기화해야 하면 : @ConfigurationPropertiesScan 필수 (환경 변수 주입이 필요하기 때문)

💡 결론 : 웬만하면 @ConfigurationPropertiesScan 사용하면서 생성자를 통해 초기화하자!!

4. .env 와 @ConfigurationProperties 사용 (TIP)

Greeting.java
java
@Getter
@Setter
@ConfigurationProperties(prefix = "greeting")
public class Greeting {
    private String name;   // GREETING_NAME 매핑
    private String coffee; // GREETING_COFFEE 매핑
}
.env
GREETING_NAME=envName
GREETING_COFFEE=envCoffee
application.properties
properties
# 실제 배포 시 환경 변수로 관리 (해당 파일이 GitHub 에 올라가야 하기 때문)
greeting.name=${GREETING_NAME:someone}
greeting.coffee=${GREETING_COFFEE:americano}

Intellij 설정 (화면 우측 상단의 editConfiguration 설정을 하면 된다.)

결과


5. 후기

🙂
기존까지는 단순히 @Value 를 쓴 경험뿐이었다. 대부분의 프로젝트나 실습 때 환경변수를 단순히 가져다만 썼기 때문이라고 생각한다.

@ConfigurationProperties 를 쓰는 방법도 알았으니 앞으로 더 복잡한 변수 관리가 필요할 때 유용하게 사용할 수 있을 것 같다.

'Spring boot' 카테고리의 다른 글

[Spring] 테스트 코드 알아보기  (1) 2026.07.22

+ Recent posts