[Spring] ConstraintValidator에 대해 알아보자

robert·2022년 2월 21일
1
post-custom-banner

서론

이번 프로젝트를 진행하면서 기본적인 유효성 처리는 java bean validation의 어노테이션을 사용하여 처리를 하였다.

그런데 이것만으로는 완벽하게 유효성을 검증할 수는 없었다.

비즈니스가 포함된 검증 로직은 유효성 검증이 불가능 하였기 때문이다.

이를 해결하기 위한 방법으로 찾다 발견한 것이 바로 ConstraintValidator 이다.

어떤 장점이 있나?

  1. 일관성있는 처리 방법 - 검증방법과 검증시점에 대해 통일성을 가질 수 있다. 이에 대해 부연 설명을 하자면... 사람마다 검증하는 단계가 컨트롤러가 될 수도 있고 서비스 레이어가 될수도 있는데 이를 validation을 사용함으로써 controller 진입 전 interceptor 단계에서 검증을 함으로써 일관성을 가질수 있다는 의미이다.

  2. 일관성 있는 ErrorResponse - 에러가 발생하면 ConstraintViolationException이 발생한다. 이를 통해 에러처리의 응답을 일관성있게 리턴할 수 있다.

어떻게 구현하나?

  1. 어노테이션을 생성한다.
@Documented
@Constraint(validatedBy = CartItemRequestValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CartItemRequestValid {
    String message() default "유효하지 않은 장바구니 담기 요청값";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. ConstraintValidator를 구현한 Validator 생성
@Component
@AllArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class CartItemRequestValidator implements ConstraintValidator <CartItemRequestValid, CartItemDto> { // 위에서 만든 annotaion 추가

    @Override
    public void initialize(CartItemRequestValid constraintAnnotation) {
    }

    @Override
    public boolean isValid(CartItemDto cartItemDto, ConstraintValidatorContext context) {

		int invalidCount = 0;

        if(cartItemDto.getOption != null && cartItemDto.getQuantity > 0) {
        	this.addConstraintViolation(context, "옵션이 존재할때 수량값은 필수입니다.", "option");
            invalidCount++;
        } else {
        ...

		return invalidCount != 0 ? false : true; //false인 경우에만 exception 발생한다.
     }
     
     private void addConstraintViolation(ConstraintValidatorContext context, String errorMessage,
                                        String firstNode) {
        context.disableDefaultConstraintViolation();
         // 검증 실패한 항목들에 대해 모두 violation을 추가할 수 있으며 이를 exception handler에서 처리가 가능하다.
        context.buildConstraintViolationWithTemplate(errorMessage)
                .addPropertyNode(firstNode)
                .addConstraintViolation();
    }
}
  1. 위에서 추가한 annotation을 검증하고자 하는 dto에 붙여준다.
@Data
@CartItemRequestValid
public class CartItemDto {
    private String option;
    private Integer quantity;
    ...
}
  1. 검증할 CartItemDto에 @RequestBody @Valid를 붙여 검증한다.
@PostMapping("/cartItem")
@ResponseBody
public ResponseEntity<CustomResponse> saveCartItem(@RequestBody @Valid CartItemDto cartItemDto) {
    ...
}
  1. 유효하지 않은 검증이 일어났을때 처리를 위해 아래처럼 handler를 구현하여 처리가 가능하다.
    ConstraintViolationException은 프로젝트 전반에 일어나는 exception이므로 controller advice로 구현하면 편하게 쓸수있다.
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public ResponseEntity<CustomResponse> handleConstraintViolationException(ConstraintViolationException constraintViolationException) {

        constraintViolationException.getConstraintViolations().stream().peek(o -> log.error(o.getMessage()));

        return new ResponseEntity<>(CustomResponse.builder()
                .message("유효하지 않은 입력값입니다.")
                .status(HttpStatus.BAD_REQUEST.value()).build() // bad request
                , HttpStatus.BAD_REQUEST);
    }
}

결론

나만의 custom validator 구현이 필요할 때는 ConstraintValidator를 사용하자.

레퍼런스

블로그-1

profile
화이팅!
post-custom-banner

0개의 댓글