ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Validator (JSR 303)
    카테고리 없음 2023. 4. 21. 15:27
    Spring validate에 대해 알아보자. 유효성 검사를 하기 위해 Spring은 org.springframework.validation.Validator 인터페이스를 제공하고 있다. Spring reference에도 잘 나와 있다. 설명이 부족한게 있다면 레퍼런스를...
    @Data
    public class Customer {
    
      private String name;
    
      private int age;
    
      private String mobilePhone;
    }
    
    이런 도메인이 있다고 가정하자. 이 중에 name은 빈값이 아니어야 하며 age 일 경우에는 0보다는 커야하며 150보다는 작아야 된다고 가정하자. 그럼 우리는 Validator 인터페이스를 활용하여 유효성 검증을 할 수 있다.
    @Component
    public class CustomerValidate implements Validator {
    
      @Override
      public boolean supports(Class<?> clazz) {
        return Customer.class.isAssignableFrom(clazz);
      }
    
      @Override
      public void validate(Object target, Errors errors) {
        Customer customer = (Customer) target;
        if (customer.getAge() < 0) {
          errors.rejectValue("age", "negativevalue");
        } else if (customer.getAge() > 150) {
          errors.rejectValue("age", "too.darn.old");
        }
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.required", "Name is required!");
      }
    }
    
    
    Validator 인터페이스를 사용해서 CustomerValidate 라는 클래스를 구현해주면 된다. Spring이니 빈으로 등록해도 된다. 보면 Validator에는 두가지 메서드를 갖고 있다. 첫 번째 메서드는 supports으로 스프링에서 많이 본 메서드 시그네쳐이다. 해당 객체가 지원가능한지 여부를 묻는 메서드이다. 두 번째 메서드는 validate라는 메서드 인데 실제 이 메서드에서 작업을 하면 된다. 예로 age는 0보다 커야 하며 150보다는 작아야 된다. 또한 name 필드는 비어있지 않아야 된다. ValidationUtils 클래스는 Spring에서 제공하는 유틸 클래스이다. rejectIfEmpty 메서드는 값이 null이거나 빈값일 경우 에러로 발생하고 " "(공백) 경우에는 통과한다. 만약 (" ")공백까지 검증하고 싶다면 rejectIfEmptyOrWhitespace 메서드를 사용하면 된다. 한번 테스트 코드를 만들어보자.
    @RestController
    @RequiredArgsConstructor
    public class CustomerController {
    
      private final CustomerValidate customerValidate;
    
      @InitBinder
      private void initBinder(WebDataBinder dataBinder){
        dataBinder.setValidator(customerValidate);
      }
    
      @PostMapping("/validTest")
      public void save(@RequestBody @Valid Customer customer, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
          FieldError fieldError = bindingResult.getFieldError();
          throw new RuntimeException(fieldError.getDefaultMessage());
        }
      }
    }
    
    CustomerValidate을 빈으로 등록했기 때문에 주입을 받을 수 있다. InitBinder 어노테이션을 활용해서 우리가 만든 customerValidate를 바인딩시키자. 또는 아래와 같이 할 수 도 있다.
    @RestController
    @RequiredArgsConstructor
    public class CustomerController {
    
      private final CustomerValidate customerValidate;
    
      @PostMapping("/validTest")
      public void save(@RequestBody Customer customer, BindingResult bindingResult){
        customerValidate.validate(customer, bindingResult);
        if(bindingResult.hasErrors()){
          FieldError fieldError = bindingResult.getFieldError();
          throw new RuntimeException(fieldError.getDefaultMessage());
        }
      }
    }
    
    
    Validate를 직접적으로 사용해도 된다. 하지만 전자가 나은듯? 뭐 아무튼 그건 개발자 마음이니.. 그리고 @Valid 어노테이션을 쓸 경우에는 바로 뒤에 BindingResult 클래스를 쓰길 권장한다. 에러처리는 되나 bindingResult가 먹히지 않는다. 그리고 원하지 않는 에러 속성들이 뷰로 전달될 것이다. 이번엔 JSR 303을 우리에 맞게 커스텀해서 만들어보자. 아주 간편하게 JSR 303을 커스텀하게 우리도 만들 수 있다. 이번에 만들 것은 모바일번호를 Validator하는 그런 어노테이션이다.
    @Target({ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = MobilePhoneValidator.class)
    public @interface MobilePhone {
    
      String message() default "{me.wonwoo.validate.mobilePhone.message}";
    
      Class<?>[] groups() default {};
    
      Class<? extends Payload>[] payload() default {};
    }
    
    MobilePhone 이라는 어노테이션을 만들었다. 음...일단 여기서 중요한건 어노테이션 메타속성인 message, groups, payload가 다 존재하야 한다. 그게 표준인거 같다. 그리고 어노테이션 위에 @Constraint 어노테이션을 사용해서 우리가 직접 Validator할 클래스를 넣어주면 된다. MobilePhoneValidator클래스를 보자.
    public class MobilePhoneValidator implements ConstraintValidator<MobilePhone, String> {
    
      @Override
      public void initialize(MobilePhone constraintAnnotation) {
      }
    
      @Override
      public boolean isValid(String value, ConstraintValidatorContext context) {
        if(value == null) {
          return false;
        }
        return value.matches("[0-9()-]*");
      }
    }
    
    ConstraintValidator 인터페이스를 구현하면 된다. ConstraintValidator인터페이스 두개의 메서드가 존재한다. initialize은 객체가 만들어 질때 최초 한번 호출되며 초기화할 데이터가 있다면 여기에서 작업하면 된다. isValid는 우리가 실제 검증할 메서드 이다. 위의 예제는 딱히 모바일을 정확하게 검증하는것은 아니고 숫자와 -(언더바) 정도만 체크한다.
    @RestController
    @RequiredArgsConstructor
    public class CustomerController {
    
      @PostMapping("/validTest")
      public void save(@RequestBody @Valid Customer customer, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
          FieldError fieldError = bindingResult.getFieldError();
          throw new RuntimeException(fieldError.getDefaultMessage());
        }
      }
    }
    
    @Data
    public class Customer {
    
      private String name;
    
      private int age;
    
      @MobilePhone
      private String mobilePhone;
    }
    
    
    이제 다시 테스트를 해보자. 딱히 Controller에 뭘 할 필요는 없고 기존과 동일하게 @Valid와 BindingResult만 넣어주자. 도메인 클래스의 해당 필드에 @MobilePhone 어노테이션을 달아주면 끝난다. 물론 원하는 메시지를 출력하고 싶다면 message속성에 메시지를 넣어주면된다.
    ...
    @MobilePhone(message = "번호가 아닙니다.")
    private String mobilePhone;
    ...
    
    만약 디폴트 메시지를 작성하고 싶다면 properties에 작성하면 된다. 위에 @MobilePhone 어노테이션의 메시지 디폴트 값이 {me.wonwoo.validate.mobilePhone.message} 이 같이 되어 있다. 보통 자기 패키지명을 따서 하길래 나도 따라해봤다.
    me.wonwoo.validate.mobilePhone.message=번호가 맞지 않습니다.
    
    이렇게 프로퍼티 파일에 작성해주면되는데 한가지 주의할 점은 properties 파일명이 ValidationMessages.properties이어야 작동한다. 아 만약 나는 저런거 만들기도 귀찮다. 물론 공통으로 써야 되는 거라면 위와 같이 어노테이션을 만들어서 해도 되지만 어떤 특정한 한 필드를 위해 만드는것은 약간 불필요 할지도 모른다. 예를 들어 age 필드는 Customer 에만 쓰는데 @Age라는 어노테이션을 만들어 0보다 커야하고 150보다 작아야 하는 그런 Validator 클래스를 구현해야 한다. 괜히 클래스만 더 늘어나는 그런현상(?)이 일어날지도 모른다. 물론 만들어도 상관은 없지만 괜히 손해인 듯 싶다. 우리는 간편하게 age필드를 검증할 수 있다.
    @Data
    public class Customer {
    
      private String name;
    
      private int age;
    
      @MobilePhone(message = "번호가 아닙니다.")
      private String mobilePhone;
    
      @AssertTrue(message = "나이는 0보다 커야 하며 150보다 작아야 합니다.")
      public boolean isValidAge(){
        return age > 0 && age < 150;
      }
    }
    
    @AssertTrue 라는 어노테이션을 해당 Validator할 메서드에 적용하면 된다. message도 입력 할 수 있다. @AssertFalse 어노테이션도 있는데 뭐 굳이..
    @Data
    public class Customer {
    
      private String name;
    
      private int age;
    
      @MobilePhone(message = "번호가 아닙니다.")
      private String mobilePhone;
    
      @AssertTrue(message = "나이는 0보다 커야 하며 150보다 작아야 합니다.")
      public boolean isValidAge(){
        return age > 0 && age < 150;
      }
    
      @AssertFalse(message = "name은 null이 아니여야 합니다")
      public boolean isValidName(){
        return name == null;
      }
    }
    
    AssertFalse을 쓰면 괜히 더 반대로 해야 되지 않나 싶다. 뭐 물론 자기가 편한대로 하면 되긴 하지만 AssertTrue가 더 한눈에 들어오는거 같다. 나는... 저 두개 말고 문서에서는 더 있던거 같았는데 뭔 문서인지 기억도 안나고 찾지도 못하겠고 내가 본건지 아니면 헛것을 봤는지 기억도 안난다. 이렇게 우리는 Spring 빈 검증에 대해서 알아봤다.

    댓글

Designed by Tistory.