Spring @Validated 가 적용되지 않는 이유
삽질의 시작 🤔
김영한 님의 스프링 부트 - 핵심원리와 활용강의의 섹션 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