Back-End/Spring

[Spring MVC2][검증(Validation)] - BindingResult(부제: 검증 오류 스프링부트와 타임리프가 손쉽게 지원하는 법, 바인딩 오류에도 400오류가 아닌 제대로 컨트롤러를 호출하는 법)

얄루몬 2022. 4. 27. 09:20

💻본 포스팅은 '스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 김영한'님의 강의를 듣고 작성되었습니다.

https://inf.run/vQHp

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com


[BindingResult]

  • 스프링이 제공하는 검증 오류 처리의 핵심이 BindingResult이다. 
  • BindingResult는 Model에 자동으로 포함됩니다.
  • 또한 검증할 대상의 바로 다음에 와야해서 파라미터로 넣어주는 순서가 중요합니다.

[컨트롤러]

@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
                            RedirectAttributes redirectAttributes) {
        
        //특정 필드의 예외
        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() > 10000) {
            bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 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}";
    }

 

  • BindingResult bindingResult를 controller의 파라미터로 추가해준다. 
    • 이때 주의해야 할 점은?
    • @ModelAttribute Item item의 다음에 BindingResult가 와야 한다.
      • 검증할 대상 바로 다음에 와야 하기때문에!!!

[thymeleaf를 사용한 뷰페이지]

<form action="item.html" th:action th:object="${item}" method="post">

        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메시지</p>
        </div>

        <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>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}"
                   th:errorclass="field-error" class="form-control" placeholder="가격을 입력하세요">
            <div class="field-error" th:errors="*{price}">
                가격 오류
            </div>
        </div>

        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}"
                   th:errorclass="field-error" class="form-control" placeholder="수량을 입력하세요">
            <div class="field-error" th:errors="*{quantity}">
                수량 오류
            </div>

        </div>
  • 타임리프 스프링 검증 오류 통합기능(타임리프는 스프링의 BindingResult를 활용하여 편리하게 검증 오류를 표현하는 기능을 제공합니다.)
    • #fields : BindingResult가 제공하는 검증 오류에 접근할 수 있다.
    • th:errors : 해당 필드에 오류가 있는 경우 태그를 출력한다. (th:if의 편의 버전인 셈이다.)
    • th:errorclass: & th:field :에서 지정한 필드에 오류가 있으면 class 정보를 추가해준다.

 

[BidingResult의 역할]

[@ModelAttribute에 바인딩 시 타입 오류 발생시에도 제대로 컨트롤러를 호출하게 한다.]

  • BindingResult가 없으면 400 오류 발생 후 컨트롤러 호출이 없이 오류 페이지로 이동한다.
    • 문제가 되는 상황으로 Integer타입만 받을 수 있는 객체에 정수가 아닌 다른 값이 들어가면 컨트롤러 호출이 안 된 채로 400페이지로 넘어가는 문제가 생긴다. 
  • BindingResult가 있으면 오류 정보(FieldError)를 BindingResult에 담아서 컨트롤러를 정상 호출한다.
    • 말 그대로 BindingResult가 있을 경우 오류 정보를 담아서 보내기에 400 에러 페이지를 보여주는 것이 아닌 컨트롤러의 정상 호출과 함께 보내주고자 하는 페이지를 제대로 보여주게 한다.

 

[BindingResult와 Errors]

  • org.springframework.validation.Errors
  • org.springframework.validation.BindingResult
  • 이 둘은 인터페이스로 Errors를 BindingResult가 상속받고 있다.
  • 실제 구현체는 BeanPropertyBindingResult이고 둘다 구현하고 있어 BindingResult대신 Errors를 사용해도 되지만 BindingResult가 더 많은 기능을 지원하고 있어 관례상 BindingResult를 더 많이 사용한다.