@Valid를 통한 Collection Dto 객체 validation
public class OrderItemRequest {
@NotNull(message = "name is not null")
@Length(max=50)
private String name;
@Min(value = 0, message = "price is over zero")
private int unitPrice;
@Min(value = 1, message = "price is over zero")
private int unitCount;
}
@PostMapping("/test")
public ApiResult<OrderDto> test(
@RequestBody @NotEmpty @Valid List<OrderItemRequest> items
) {
// 생략
}
- 먼저 Collection을 @Valid 아래와 같이 했을때 원했던 동작 결과는 OrderItemRequest의 @NotNull, @Length, @Min 등의 Valid를 하고 싶었지만, 정상적으로 동작하지 않았다.
- 그 이유를 찾아보기 위해 디버깅을 통해 클래스를 확인해보니
- RequestResponseBodyMethodProcessor.validateIfApplicable()
- @Valid 어노테이션 확인
- DataBinder.validate()
- org.springframework.validation.DataBinder
- ValidatorImpl.validate()
- org.hibernate.validator.internal.engine.ValidatorImpl
@Override
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
sanityCheckGroups( groups );
@SuppressWarnings("unchecked")
Class<T> rootBeanClass = (Class<T>) object.getClass();
BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
if ( !rootBeanMetaData.hasConstraints() ) {
return Collections.emptySet();
}
BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );
ValidationOrder validationOrder = determineGroupValidationOrder( groups );
BeanValueContext<?, Object> valueContext = ValueContexts.getLocalExecutionContextForBean(
validatorScopedContext.getParameterNameProvider(),
object,
validationContext.getRootBeanMetaData(),
PathImpl.createRootPath()
);
return validateInContext( validationContext, valueContext, validationOrder );
}
- ValidatotrImpl.validate()에서
if ( !rootBeanMetaData.hasConstraints() )
제약 조건에 걸리면서 emptySet()을 반환하고 종료된다. - Collection이 JavaBeans 명세에 포함되지 않아서 그렇다.
해결방법
- 이 해결 방법은 spring boot 2.x 이상 버전에서만 정상 동작한다.
- 이게 아니면 직접 custom validator를 구현하자
@RestController
@Validated // 이 어노테이션을 클래스 레벨에 붙여 준다.
public class Apis {
// 메소드는 기존과 동일하다.
@PostMapping("/test")
public ApiResult<OrderDto> test(
@RequestBody @NotEmpty @Valid List<OrderItemRequest> items
) {
// 생략
}
}
동작방식
- 위의 기본적인 @Valid 경우를 확인한다 -> 여기서 emptySet()을 반환하기 떄문에 에러가 없다.
- MethodValidationInterceptor
- org.springframework.validation.beanvalidation.MethodValidationInterceptor
- ValidatorImpl.validateParameters()
- org.hibernate.validator.internal.engine.ValidatorImpl
- validateParameters를 호출하면서 Collection 내부의 Dto들에 대해 validate를 진행한다.
Exception Handling
예외 핸들링 방법으로는 @ControllerAdvice 방식을 사용했다.
@RestControllerAdvice public class ControllerExceptionAdvice { @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity error(ConstraintViolationException e) { return ResponseEntity.ok().body(ApiResult.failed(e.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity error(MethodArgumentNotValidException e) { return ResponseEntity.ok().body(ApiResult.failed(e.getBindingResult().getAllErrors().get(0).getDefaultMessage())); } }
ConstraintViolationException 예외는 Collection을 validateParameters()에서 발생하는 예외이다.
MethodArgumentNotValidException 예외는 먼저 동작하는 validate()에서 발생하는 예외이다.
- bindingResult에 값이 담겨 오기 때문에 풀어서 적어줬다.
문제점
- 기본 validate(), validateParameters() 함수를 두번 부른다.
- 그냥 Dto를 validate할때도 마찬가지로 두번을 부른다
- 그냥 CustomValidator를 구현하는 방법으로 변경해야 할지 의문이 생긴다.
Custom Validator
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import org.springframework.web.bind.MethodArgumentNotValidException;
import javax.validation.Validation;
import java.util.Collection;
@Component
public class CustomCollectionValidator implements Validator {
private SpringValidatorAdapter validator;
public CustomCollectionValidator() {
this.validator = new SpringValidatorAdapter(
Validation.buildDefaultValidatorFactory().getValidator()
);
}
@Override
public boolean supports(Class clazz) {
return true;
}
@Override
public void validate(Object target, Errors errors) {
if(target instanceof Collection){
Collection collection = (Collection) target;
for (Object object : collection) {
validator.validate(object, errors);
}
} else {
validator.validate(target, errors);
}
}
}
위와 같이 CustomValidator를 만들어 준뒤 controller class에 DI 받아온다
private final CustomCollectionValidator customCollectionValidator;
// 생성자 코드 생략
@PostMapping("test")
public ApiResult<OrderDto> createOrder(
@RequestBody @Valid List<OrderItemRequest> items, BindingResult bindingResult
) {
customCollectionValidator.validate(items, bindingResult);
if(bindingResult.hasError()) {
//error 처리
}
//생략
}
문제점
- Apis 메소드에 추가되는 코드가 생긴다.
결과
- 개인적으로는 클래스에 @Validated를 붙이는 방식이 낫다고 생각한다.
- 직관적이지는 않지만 새로 추가되는 코드가 없어 유지보수성이 높다고 생각합니다
- 수정
- 다른 DTO들도 validate() + validateParameter()를 두번 타는 비효율을 가질 필요가 없다고 생각해 CustomValidator를 사용하는 방법으로 변경하였다.
더 깔끔한 방법은 뭘까... 정답을 아시면 알려주세요..
'Spring' 카테고리의 다른 글
Spring Batch 삽질기 1탄 (0) | 2021.06.22 |
---|---|
SpringBatch Jpa Test 설정 (0) | 2021.06.22 |
Spring Bean 생명주기 (0) | 2021.06.22 |
스프링 IoC, DI (0) | 2021.06.22 |
Spring 이란 (1) | 2021.06.22 |
최근댓글