ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC - 스프링에서 제공하는 검증(Validation) 방법 (FieldError, ObjectError)
    Spring/Spring MVC 2022. 2. 5. 23:21
    반응형

     

     

     

     

     

     

    스프링에서 제공하는 검증(Validation) 방법

     

    BindingResult

     

    • 스프링에서 제공하는 검증 방법의 핵심
    • 검증 오류를 보관하는 객체, 검증 오류가 발생 시 BindingResult에 보관
    • Model에 자동으로 저장해준다.
    • 바인딩시 데이터 타입 오류가 발생해도 컨트롤러를 호출해 준다.
         - BindingResult 미사용  →  400 오류 페이지로 이동된다.
         - BindingResult 사용     →  오류 정보(FieldError)를 BindingResult에 담아서 컨트롤러 호출
    • BindingResult에 검증 오류를 적용하는 3가지 방법
           1. @ModelAttribute의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이
              FieldError를 생성하여 BindingResult에 넣어준다.
           2. 개발자가 직접 addError 메서드를 통해 넣어준다.
           3. Validator 사용

     

     

    BindingResult와 Errors

     

     BindingResultErrors 인터페이스를 상속받은 인터페이스다. 실제로 사용하는 구현체는 BeanPropertyBindingResult 클래스이므로 BindingResultErrors 모두 사용해도 되지만 Errors 인터페이스는 단순한 오류 저장과 조회 기능만 제공하고, BindingResult는 추가적인 기능을 제공한다(addError 메서드 등). 따라서 주로 BindingResult를 많이 사용한다.


     

     

    검증 기능 구현(Spirng에서 제공하는 검증 기능 사용)

     

    Controller에서 검증 기능 구현

     

     다음과 같은 검증 기능을 구현해보자.

    • 타입 검증
         - 가격, 수량에 문자가 들어가면 검증 오류 처리
    • 필드 검증
         - 상품명: 필수, 공백 X
         - 가격: 1000원 이상, 1백만원 이하
         - 수량: 최대 9999
    • 특정 필드의 범위를 넘어서는 검증
         - 가격 * 수량의 합은 10,000원 이상



    1. BindingResult@ModelAttribute 객체 다음에 와야 한다.
      public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {​
    2. 필드가 검증에 위반되는 경우 BindingResultFiledError 객체를 담아준다.
      if(!StringUtils.hasText(item.getItemName())){
          bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수 입니다."));
      }
      if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
          bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
      }
      if (item.getQuantity() == null || item.getQuantity() >= 9999) {
          bindingResult.addError(new FieldError("item", "price", "수량은 최대 9,999 까지 허용 합니다."));
      }
      
      //특정 필드가 아닌 복합 룰 검증
      if (item.getPrice() != null && item.getQuantity() != null) {
          int resultPrice = item.getPrice() * item.getQuantity();
          if (resultPrice < 10000) {
              bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재값 = " + resultPrice));
          }
      }​
      필드 오류 : FieldError
            - public FieldError(String objectName, String field, String defaultMessage) {}
                    objectName      : @ModelAttribute 이름
                    field                : 오류가 발생한 필드 이름
                    defaultMessage : 기본 오류 메시지 
       글로벌 오류 : ObjectError
            - public ObjectError(String objectName, String defaultMessage) {}​
                    objectName      : @ModelAttribute 이름
                    defaultMessage : 기본 오류 메시지 

    3. 검증에 위반된 것이 하나라도 있을 시 입력 폼으로 
      if (bindingResult.hasErrors()) {
          log.info("errors={}", bindingResult);
          return "validation/v2/addForm";
      }​
      hasErrors  - errors가 있으면 true, 없으면 false 반환 

    4. 전체 코드
      @PostMapping("/add")
      public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
      
          //검증 로직
          if(!StringUtils.hasText(item.getItemName())){
              bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수 입니다."));
          }
          if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
              bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
          }
          if (item.getQuantity() == null || item.getQuantity() >= 9999) {
              bindingResult.addError(new FieldError("item", "price", "수량은 최대 9,999 까지 허용 합니다."));
          }
      
          //특정 필드가 아닌 복합 룰 검증
          if (item.getPrice() != null && item.getQuantity() != null) {
              int resultPrice = item.getPrice() * item.getQuantity();
              if (resultPrice < 10000) {
                  bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재값 = " + resultPrice));
              }
          }
      
          //검증에 실패하면 다시 입력 폼으로
          if (bindingResult.hasErrors()) {
              log.info("errors={}", bindingResult);
              return "validation/v2/addForm";
          }
      
          //성공 로직
      
          Item savedItem = itemRepository.save(item);
          redirectAttributes.addAttribute("itemId", savedItem.getId());
          redirectAttributes.addAttribute("status", true);
          return "redirect:/validation/v2/items/{itemId}";
      }​

     

     

    검증 위반에 따른 errors 메시지 출력

     

     서버단에서 검증 구현을 하였다. 이제 검증에 위반하였을 때 화면에 해당 검증 위반 메시지가 화면에 나타나도록 HTML을 수정해보자.

     

    1. 위에서 구현한 검증 중 글로벌 에러(ObjectError)가 있을 시 화면에 에러 메시지 출력
      <div th:if="${#fields.hasGlobalErrors()}">
          <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메시지</p>
      </div>​
         서버를 가동시켜 웹브라우저에서 HTML의 소스 코드를 보면 다음과 같다.
      <div>
          <p class="field-error">가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재값 = 1</p>
      </div>​
      #fields : BindingResult에 저장된 errors에 접근할 수 있다.       
              - #fields.hasGlobalErrors() : 글로벌 errors 가 있으면 true, 없으면 false
              - #fields.globalErrors() : 글로벌 에러(ObjectError)를 List 자료형으로 가져옴

    2. 위에서 구현한 검증 중 필드 오류(FieldError)가 있을 시 화면에 에러 메시지 출력
      (price, quantity 같은 방식이므로 생략)
      <div>
          <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
          <input type="text" id="itemName" th:field="*{itemName}"
                 th:errorclass="field-error"
                 class="form-control" placeholder="이름을 입력하세요">
          <div class="field-error" th:errors="*{itemName}">
              상품명 오류
          </div>
      </div>
        서버를 가동시켜 웹브라우저에서 HTML의 소스 코드를 보면 다음과 같다.
      <div>
          <label for="itemName">상품명</label>
          <input type="text" id="itemName" class="form-control field-error" placeholder="이름을 입력하세요" name="itemName" value="">
          <div class="field-error">상품 이름은 필수 입니다.</div>
      </div>
         th:errorclass="field-error"
              - 해당 필드에 error가 있을 경우 class 속성에 field-error 추가
              - ex) errors 있는 경우  →    class="field-error field-error"
      th:errors="*{itemName}"
              - 해당 필드에 error가 있을 경우 error message 출력
              - th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}" 와 같다   

     

    참고

    Thymeleaf - validation-and-error-messages docs

    Tutorial: Thymeleaf + Spring

     


     

     위 예시 코드로 검증 로직을 구현하였지만 문제가 있다. 검증에 위반하였을 때 다시 입력 폼으로 돌아가면 입력했던 값들이 유지가 되지 않고 사라진 것을 볼 수 있다. 만약 웹 페이지 사용자가 자신이 값이 검증 오류가 발생하여 다시 입력할때 입력했던 값이 사라져 자신이 입력한 값이 무엇인지 확인할 수 없게 된다.

     

    입력 폼
    검증에 위반하여 다시 입력 폼으로 돌아왔을 때 입력했던 값이 사라졌다

     

     

     

     

     

      그렇다면 검증에 위반하였을 때 입력된 값을 유지하는 방법을 알아보자.

     

     

    FieldError, ObjectError

     

    FieldError 의 생성자

    public FieldError(String objectName, String field, String defaultMessage) {...}
    public FieldError(String objectName, String field, 
                      @Nullable Object rejectedValue, boolean bindingFailure, 
                      @Nullable String[] codes, @NullableObject[] arguments, 
                      @Nullable String defaultMessage) {...}
    • objectName    : 오류가 발생항 객체 이름
    • field              : 오류 필드
    • rejectedValue  : 사용자가 입력한 값(거절된 값)
    • bindingFailure : 타입 오류 같은 바이딩 실패인지, 검증 실패인지 구분 값
    • codes            : 메시지 코드
    • arguments      : 메시지에서 사용하는 인자
    • defaultMessage : 기본 오류 메시지

     

    ObjectError 의 생성자

    public ObjectError(String objectName, String defaultMessage) {...}
    public ObjectError(
            String objectName, @Nullable String[] codes, 
            @Nullable Object[] arguments,
            @Nullable String defaultMessage) {...}
    • objectName    : 오류가 발생항 객체 이름
    • codes            : 메시지 코드
    • arguments      : 메시지에서 사용하는 인자
    • defaultMessage : 기본 오류 메시지

     

     즉, 입력된 값을 유지하기 위해서는 FieldError의 생성자를 위 예시에서 사용한 생성자가 아닌 사용자가 입력한 값을 저장하는 필드(rejectedValue)가 있는 생성자를 사용해주면 된다.

     

    변경된 전체 코드

    //검증 로직
    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수 입니다."));
    }
    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
    }
    if (item.getQuantity() == null || item.getQuantity() >= 9999) {
        bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, null ,null, "수량은 최대 9,999 까지 허용합니다."));
    }
    
    //특정 필드가 아닌 복합 룰 검증
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.addError(new ObjectError("item",null ,null, "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
        }
    }

     

    타임리프의 사용자 입력 값 유지

     

     th:field 는 검증에 통과했을 때는 th:obejct 의 필드 값을 사용하지만, 검증 오류가 발생하면 FieldError에서 보관된 rejectedValue 값을 이용하여 값을 출력한다.


     

     

     

     기본적인 기능을 알아보았다. 추가 궁금한 기능은 해당 링크를 참조하길 바란다

    Tutorial: Thymeleaf + Spring

     

    Tutorial: Thymeleaf + Spring

    Preface This tutorial explains how Thymeleaf can be integrated with the Spring Framework, especially (but not only) Spring MVC. Note that Thymeleaf has integrations for both versions 3.x and 4.x of the Spring Framework, provided by two separate libraries c

    www.thymeleaf.org

     

     

    참조 강의

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 학습 페이지 (inflearn.com)

     

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 학습 페이지

    지식을 나누면 반드시 나에게 돌아옵니다. 인프런을 통해 나의 지식에 가치를 부여하세요....

    www.inflearn.com

    반응형

    댓글

Designed by Tistory.