모르지 않다는 것은 아는것과 다르다.

Spring

BindingResult

채마스 2022. 2. 26. 02:16

BindingResult

  • BindingResult는 검증 오류가 발생할 경우 오류 내용을 보관하는 스프링 프레임워크에서 제공하는 객체다.
  • 스프링이 제공하는 검증 오류를 보관하는 객체이다.
  • 검증 오류가 발생하면 여기에 보관하면 된다.
  • BindingResult 파라미터의 위치는 @ModelAttribute 파라미터 다음에 와야한다.
  • BindingResult가 없으면 @ModelAttribute 에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다.
    • 원래 같았으면 400에러로 오류 페이지로 넘어가야 되지만, BindingResult를 넣어주면 오류에 대한 결과가 BindingResult 객체에 담기고 컨트롤러가 호출된다.
  • BindingResult에 검증 오류를 적용하는 3가지 방법
    • @ModelAttribute의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 FieldError를 생성해서 BindingResult에 넣어준다.
    • 개발자가 오류를 직접 넣는다.
    • Validator를 사용한다.
  • 타입 오류로 바인딩에 실패하면 스프링이 FieldError를 생성하면서 사용자가 입력한 값을 넣어둔다. -> 그리고 해당 오류를 BindingResult에 담아서 컨트롤러를 호출한다. -> 이렇게 되면 타입 오류같은 바인팅 실패시에도 사용자의 오류 메시지를 정상적으로 출력할 수 있다.
  • 위에서 언급한 FieldError는 아래와 같은 파라미터들을 갖는다.
  public FieldError(String objectName, String field, String defaultMessage);
  public FieldError(String objectName, String field, @Nullable Object rejectedValue, 
                  boolean bindingFailure, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage)
  • objectName : 오류가 발생한 객체 이름
  • field : 오류 필드
  • rejectedValue : 사용자가 입력한 값
  • bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
  • codes : 메시지 코드
  • arguments : 메시지에서 사용하는 인자
  • defaultMessage : 기본 오류 메시지
    • 이 중에서 codes, arguments 는 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.
  • src/main/resources/errors.properties 를 추가하고 아래와 같이 사용할 수 있다.위와 같이 errors.properties를 작성했다면 아래와 같이 사용할 수 있다.
  • range.item.price=가격은 {0}~{1}까지 허용합니다.
  bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 100000}, null));
  • bindingResult.getObject() : target 객체의 이름
  • bindingResult.getTarget() : target 객체
  • bindingResult.rejectValue("price", "range", new Object[]{1000, 100000}, null) : rejectValue를 사용하면 FieldError를 더 간단하게 구현할 수 있다.
  • 구현 예시는 아래와 같다.
  @PostMapping("/add")
  public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult,
  RedirectAttributes redirectAttributes) {
      log.info("objectName={}", bindingResult.getObjectName());
      log.info("target={}", bindingResult.getTarget());
      if (!StringUtils.hasText(item.getItemName())) {
          bindingResult.rejectValue("itemName", "required");
    }    
      if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
          bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
      }
      if (item.getQuantity() == null || item.getQuantity() > 10000) {
          bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);
      }
      //특정 필드 예외가 아닌 전체 예외
      if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
      } 
    }
      if (bindingResult.hasErrors()) {
          log.info("errors={}", bindingResult);
          return "validation/v2/addForm";
    }
  }
  • 위에 "range"는 MessageCodesResolver 인터페이스 덕분에 error,properties에 정의된 메시지를 사용할 수 있는 것이다.
  • validation 클래스를 따로 분리해서 구현하면 아래와 같이 구현할 수 있다.
  @Component
  public class ItemValidator implements Validator {
      @Override
      public boolean supports(Class<?> clazz) {
          return Item.class.isAssignableFrom(clazz);
      }

      @Override
      public void validate(Object target, Errors errors) {
          Item item = (Item) target;
          ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");

          if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
              errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
          }
          if (item.getQuantity() == null || item.getQuantity() > 10000) {
              errors.rejectValue("quantity", "max", new Object[]{9999}, null);
          }
      }
  }
  • @Component 를 달아줘서 빈에 등록시킨 후 검증기에서 사용할 수 있다.
  • 검증기를 구현해서 컨트롤러 호출시 무조건 실행되게 설정할 수 있다. -> 컨트롤러를 매번 호출할때마다 검증하는 코드를 작성할 필요가없다. 구현은 아래와 같이 할 수 있다.
    @InitBinder
    public void init(WebDataBinder dataBinder){
        dataBinder.addValidators(itemValidator);
    }
  • 위와 같이 검증기를 사용하기 위해서는 컨트롤러에 @Validated를 적어줘야 한다.
  • @Validated 는 검증기를 실행하라는 애노테이션이다. -> WebDataBinder에 등록된 검증기를 찾아서 실행한다.
  • @Valid 를 사용해도 된다.
    • @Validated 는 스프링 전용 애노테이션이고, @Valid 는 자바 전용 애노테이션이다.




REFERENCES

  • 김영한님의 스프링 MVC 2편

'Spring' 카테고리의 다른 글

Transactional Propagation  (0) 2022.02.26
SpringMVC  (0) 2022.02.26
Bean Validation (BindingResult 개념을 먼저 숙지 해야된다.)  (0) 2022.02.26
ApplicationRunner,CommandLineRunner  (0) 2022.02.26
API 예외처리  (0) 2022.02.26