본문 바로가기

개발/자바&스프링

@ConfigurationProperties 바인딩 동작 원리 분석 & SpringBoot 3.x 에서 변경된 내용

1. Intro
2. 맥락
3. 원인 분석
4. 해결 방법

 

Intro

 

환경

  • Spring Boot 3.1.1
  • spring-boot-starter-jdbc 3.1.1

스프링 부트에서 @ConfigurationProperties 선언 시 내부적으로 설정값을 주입하는 방법과

스프링 부트 3.x 에서 변경된 @ConfigurationBinding 내용을 모른 채로 DataSourceProperties를 상속해 작업하다 예외가 발생했다. 

 

@ConfigurationProperties를 빈 팩토리가 어떻게 처리는 지 파악하고 예외를 어떻게 해결했는지 공유해 보겠다. 

 

 

맥락

 

DB 환경 설정 정보를 Vault 서버에서 읽어와야 했다.

Vault 서버에서 읽어온 정보를 설정 클래스에 바인딩 작업을  DataSourceProperties를 상속한 클래스에서 작업했다. 

 

 

VaultTemplate 조회 역할을 하는 VaultOperationService를 생성자 주입하고 빌드를 했으나 예외가 발생했다. 

Cannot bind @ConfigurationProperties for bean 'rdsConnectionProps'. Ensure that @ConstructorBinding has not been applied to regular bean

 

에러가 의미하는 것은

bean에 ConfigurationProperties 값을 바인딩할 수 없으니 Bean에 @ConstructorBinding을 적용하지 말라는 것이다. 

@ConstructorBinding을 선언하지 않았는데 왜 이런 예외 메시지가 출력되었을까?

 

원인 분석

원인을 파악하기 위해 스프링부트 코드를 분석해 보자.

 

 

@ConfigurationProperties에 선언된 prefix에 해당하는 환경설정값을 Bean으로 바인딩은 빈 후처리기가 담당한다. 

 

 

1. BeanFactory 가 Bean 객체 생성

2. ConfigruationPropertiesBindingPostProcessor에서 바인딩 처리

 

  • ConfigruationPropertiesBindingPostProcessor의 바인딩 작업에는 두 객체가 참여한다.
    • ConfigurationPropertiesBean :
      • @ConfigurationProperties이 선언된 빈의 참조값을 래핑 하는 객체다. 참조값뿐만 아니라 Bindable 객체도 보관한다.
      • Bindable 은 바인딩할 타깃 빈의 어노테이션 정보나 bind 방법 등 binder 가 어떻게 바인딩해야 할지 정보를 참고하는 용도라고 보면 된다.
      • 클래스 메서드로는 applicationContext에 등록된 ConfigurationPropertiesBean 조회 기능을 제공한다.
    • ConfigurationPropertiesBinder : ConfigruationPropertiesBindingPostProcessor 내부에서 바인딩 작업을 처리한다. 
      • 아래 UML을 보면 1:1 합성 관계를 맺고 있다. 

 

3. ConfigruationPropertiesBindingPostProcessor는 ConfigurationPropertiesBinder에 바인딩을 요청하기 전 ConfigurationPropertiesBean의 상태 검사를 한다. 

 

  • Assert.state 문에서 표현식이 false 여서 예외를 뱉는다. 내가 만난 예외가 여기서 발생했다. 
    • BindMethod 가 VAULE_OBJECT 면 바인딩 작업이 불가능하다.
    • 위에서 Bindable 객체는 바인딩 방법에 대한 정보를 보관한다고 언급했는데, 이 객체에 BindMethod 가 존재한다.
      • BindMethod 에는 수정자 메서드로 바인딩을 뜻하는 JAVA_BEAN과 생성자로 바인딩을 뜻하는 VAULE_OBJECT 가 존재한다
    • ConfigruationPropertiesBindingPostProcessor.bind() 메서드에서는 getter/setter 로만 바인딩이 가능하다.
/**
 * Configuration property binding methods.
 *
 * @author Andy Wilkinson
 * @since 3.0.8
 */
public enum BindMethod {

	/**
	 * Java Bean using getter/setter binding.
	 */
	JAVA_BEAN,

	/**
	 * Value object using constructor binding.
	 */
	VALUE_OBJECT;

}

 

 

예외 원인 정리

 

 

예외 원인은 내가 선언한 빈의 생성자 때문에 BindMethod이 생성자 바인딩인 VALUE_OBJECT로 세팅이 되었다. 

BindMethod 이 결정되는 부분은 위에서 언급했듯이 환경설정 빈의 참조값과 Bindable 객체를 들고있는 ConfigurationPropertiesBean 에서 deduceBindMethod 라는 메서드가 바인딩 방법을 추론하여 결정한다. 

 

 

이렇게 된 이유는 Spring Boot 3부터는 Constructorbinding을 사용하기 위해서 @EnableConfigurationProperties 나 @ConfigurationPropertiesScan 선언이 필요 없어졌으며,

환경설정 빈에 매개변수 있는 생성자 1개만 존재하면 생성자 바인딩임을 암묵적으로 명시한다고 했다. 단, 이 생성자에 @Autowired가 없어야 한다.

 

정리하면 환경설정 빈에 의존관계 주입용으로 선언한 생성자가 내 의도와 다르게 내부적으로는 생성자 바인딩용으로 인지된 것이다. 

 

 

해결 방법

 

생성자에  @Autowired 를 선언해 생성자 바인딩용 생성자가 아님을 알려줘야 한다.

 

@Primary
@Configuration
@Profile(value = {"dev"})
public class RdsConnectionProps extends DataSourceProperties {

    private final VaultOperationService vaultOperationService;

    @Autowired
    public RdsConnectionProps(VaultOperationService vaultOperationService) {
        this.vaultOperationService = vaultOperationService;
    }

 

 

느낀 점

- 구현 코드를 디버깅하며 매우 많은 스택 트레이스 리스트 때문에 반강제로 객체의 역할, 책임, 협력 관계를 분석해야 흐름을 파악을 할 수 있었다. 이 과정에서 좋은 OOP 예시는 프레임워크나 라이브러리 구현 코드를 까보면 되는구나 느꼈다.

- 스프링부트 버전에 따른 변경 내용을 자주 트래킹해야 됨을 느꼈다. 

 


참고

Spring boot 2.x 에서 ConstructorBinding 동작 : https://codingdog.tistory.com/entry/spring-boot-constructorbinding-%EC%83%9D%EC%84%B1%EC%9E%90%EB%A1%9C-binding%EC%9D%84-%EC%8B%9C%ED%82%A8%EB%8B%A4