Develop/✍️ 삽질기

Spring @Validated 가 적용되지 않는 이유

인피니 2023. 12. 24. 18:59

삽질의 시작 🤔

김영한 님의 스프링 부트 - 핵심원리와 활용강의의 섹션 7에서 @ConfigurationProperties에 대해 학습하던 중 의문이 한 가지 생겼다.

@ConfigurationProperties 은 자바 빈 검증기를 사용할 수 있다는 것이다. 

@Getter
@ConfigurationProperties("my.datasource")
@Validated
public class MyDataSourcePropertiesV3 {

    @NotEmpty
    private String url;
    @NotEmpty
    private String username;
    @NotEmpty
    private String password;
    private Etc etc;

    public MyDataSourcePropertiesV3(String url, String username, String password, Etc etc) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.etc = etc;
    }

    @Getter
    public static class Etc {
        @Min(1) @Max(999)
        private int maxConnection;
        @DurationMin(seconds = 1)
        @DurationMax(seconds = 60)
        private Duration timeout;
        private List<String> options = new ArrayList<>();

        public Etc(int maxConnection, Duration timeout, List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }


}

(강의 코드를 가져옴!)

위 코드처럼 @Validated 어노테이션을 클래스 레벨에 사용하고 @Min,@Max 등 어노테이션을 사용하면 각 필드에 들어오는 값에 따라 빈 검증기가 동작하게 된다.!

 

그렇다면...! 

@ConfigurationProperties를 사용하지 않고 properties 값을 가져오는 객체에서도 @Validated와 @Max 이런 식으로 사용하면 검증기가 동작하지 않을까? 

@Slf4j
@Configuration
@Validated
public class MyDataSourceValueConfig {
    @Value("${my.datasource.url}")
    private String url;
    @Value("${my.datasource.username}")
    private String username;
    @Value("${my.datasource.password}")
    private String password;
    @Value("${my.datasource.etc.max-connection}")
    @Max(10)
    private int maxConnection;
    @Value("${my.datasource.etc.timeout}")
    private Duration timeout;
    @Value("${my.datasource.etc.options}")
    private List<String> options;

    @Bean
    public MyDataSource myDataSource1() {
        return new MyDataSource(url,username,password,maxConnection,timeout,options);
    }

    @Bean
    public MyDataSource myDataSource2(
            @Value("${my.datasource.url}") String url,
            @Value("${my.datasource.username}") String username,
            @Value("${my.datasource.password}") String password,
            @Value("${my.datasource.etc.max-connection}") int maxConnection,
            @Value("${my.datasource.etc.timeout}") Duration timeout,
            @Value("${my.datasource.etc.options}") List<String> options) {
        return new MyDataSource(url, username, password, maxConnection, timeout, options);
    }
}

-> 이렇게 작성했는데 @Max 어노테이션이 동작하지 않는 거 같다...? 즉 빈 검증기가 동작하지 않는 거 같다. 왜지? 

 

1) 해결과정 @Validated & @ConfigurationProperties 확인해보기 

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {

	/**
	 * Specify one or more validation groups to apply to the validation step
	 * kicked off by this annotation.
	 * <p>JSR-303 defines validation groups as custom annotations which an application declares
	 * for the sole purpose of using them as type-safe group arguments, as implemented in
	 * {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
	 * <p>Other {@link org.springframework.validation.SmartValidator} implementations may
	 * support class arguments in other ways as well.
	 */
	Class<?>[] value() default {};

}

 

 

딱히 문제의 원인이 될만한 코드가 보이지 않는다.

Validated 가 언급된 코드들을 살펴본다.

커서가 있는 부분의클래스명을 보면 ConfigurationProperties랑 연관이 있어 보이는 클래스를 확인할 수 있다.

해당 클래스로 가보면 

/*
 * Copyright 2012-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.context.properties;

import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

/**
 * Validator that supports configuration classes annotated with
 * {@link Validated @Validated}.
 *
 * @author Phillip Webb
 */
final class ConfigurationPropertiesJsr303Validator implements Validator {

	private static final String[] VALIDATOR_CLASSES = { "jakarta.validation.Validator",
			"jakarta.validation.ValidatorFactory", "jakarta.validation.bootstrap.GenericBootstrap" };

	private final Delegate delegate;

	ConfigurationPropertiesJsr303Validator(ApplicationContext applicationContext) {
		this.delegate = new Delegate(applicationContext);
	}


	@Override
	public void validate(Object target, Errors errors) {
		this.delegate.validate(target, errors);
	}

}

ConfiguraionPropertiesJsr303Validator 클래스의 일부 코드만 가져왔다.

주석을 확인해보면 @Validated 어노테이션이 있는 configuration class를 지원하는 검증기인 것을 확인할 수 있다.

@Validated에 대한 탐색을 멈추고, @ConfigurationProperties 로 넘어가자! 

 

@ConfigurationProperties와 관련이 있는 클래스들을 살펴보자 

-> 해당 클래스에 들어가보면 

 

	Object bindOrCreate(ConfigurationPropertiesBean propertiesBean) {
		Bindable<?> target = propertiesBean.asBindTarget();
		ConfigurationProperties annotation = propertiesBean.getAnnotation();
		BindHandler bindHandler = getBindHandler(target, annotation);
		return getBinder().bindOrCreate(annotation.prefix(), target, bindHandler);
	}

-> bind 객체를 만들어 줌 

	private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
		List<Validator> validators = getValidators(target);
		BindHandler handler = getHandler();
		handler = new ConfigurationPropertiesBindHandler(handler);
		if (annotation.ignoreInvalidFields()) {
			handler = new IgnoreErrorsBindHandler(handler);
		}
		if (!annotation.ignoreUnknownFields()) {
			UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
			handler = new NoUnboundElementsBindHandler(handler, filter);
		}
		if (!validators.isEmpty()) {
			handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
		}
		for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
			handler = advisor.apply(handler);
		}
		return handler;
	}

validators를 넣어주는 것을 볼 수 있음 

-> 커서에 있는 if 조건들을 확인 후, 검증기를 관리하는 validators 변수에 검증기를 넣어줌

 

 

-> validator.validate 실행 후 에러가 발생하면 this.exception 에 담아주는데 

 

Binding validation errors on my.datasource.etc
   - Field error in object 'my.datasource.etc' on field 'maxConnection': rejected value [0]; codes [Min.my.datasource.etc.maxConnection,Min.maxConnection,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [my.datasource.etc.maxConnection,maxConnection]; arguments []; default message [maxConnection],1]; default message [1 이상이어야 합니다]; origin class path resource [application.yml] - 13:23

 

위와 같은 msg 를 담아준다.

 

애플리케이션 실행시 검증조건에 맞지 않으면 아래 사진과 같은 메시지가 콘솔에 찍히는데 

위에서 담긴 메시지의 내용에서 일부 발췌되어 출력되는 거 같다...?

 

@Validated 만 등록하면, 빈 검증기가 동작하는 줄 알았는데, 아닌 것인가...?  실 코드를 보니 validator.validate() 메서드가 실행되고, 검증 로직이 동작하는 거 같음 

 

2) 해결과정 @Validated 개념 확인 

영한님의 spring boot mvc 2 편에 있는 검증기부분을 다시 확인해보니 

@Validated는 검증기를 실행하라는 애노테이션이며, 검증기를 찾아 validate() 메서드를 실행하라는 것을 알 수 있게 되었다.

즉,,, @Validated 어노테이션만 있다고 검증기가 자동으로 동작하는 것이 아니라는 것이다.

 

위에서 @ConfigurationProperties 애노테이션을 사용해 빈 검증기를 사용한 코드들도, 위에서 확인해 본 것처럼 validator.validate() 메서드가 호출되도록, 구현되어 있는 것 

 

 

삽질과정을 통해 @Validated 개념을 확실하게 잡을 수 있었다. 😊

보통 사용하는 @Validated 애노테이션을 사용해 빈 검증기를 동작시키는 것은 어디에선가, 해당 검증기를 실행하도록 validate() 메서드를 호출하는 코드가 구현되어 있을 것이다.

영한님의 강의를 들으면 종종 들을 수 있는 말이 용빼는 재주가 없다는 것인데, @Validated 도 사실 편리성을 위해 만들어진 애노테이션이지 해당 애노테이션만을 사용해 검증기를 호출할 용빼는 재주는 없다는 것! 👍

 

👉 참고: https://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/html/validation.html 

 

7. Validation, Data Binding, and Type Conversion

There are pros and cons for considering validation as business logic, and Spring offers a design for validation (and data binding) that does not exclude either one of them. Specifically validation should not be tied to the web tier, should be easy to local

docs.spring.io

👉 참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2