Spring 검증 2

바그다드·2023년 5월 9일
0

검증

목록 보기
2/5
  • 지난 포스팅에서 간단한 검증을 구현해 봤는데, 몇가지 문제가 있었다.
    그 중에서 데이터 타입이 맞지 않을 경우 컨트롤러를 호출하기 전에 에러가 발생한다.
    이러한 검증은 스프링에서 기능을 제공하는데, 바로 BindingResult이다.
    먼저 코드로 확인해보자

컨트롤러

    @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", "quantity", "수량은 최대 9,999까지 허용합니다."));
        }

        // 특정 필드가 아닌 복합 룰 검증
        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                // 특정 필드 에러가 아닌 글로벌 에러이므로 ObjectError로 담아줌
                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의 위치는 @ModelAttribute객체 바로 다음에 와야한다.
    bindingResult가 지난 포스팅의 errors 역할을 한다.
  • bindingResult는 자동으로 view로 넘어가기 때문에 따로 model에 담아주지 않아도 된다.
  • FieldError는 필드에 에러가 있을 때 사용하며, 파라미터로
    1. objectName : @ModelAttribute 이름
    2. field : 오류가 발생한 필드 이름
    3. defaultMessage : 오류 기본 메시지
    를 받는다.
  • ObjectError는 필드 이상의 글로벌 에러가 있을 때 생성하는데, 파라미터로
    1. objectName : @ModelAttribute 의 이름
    2. defaultMessage : 오류 기본 메시지
    를 받는다.

view 수정

<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>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/validation/v2/items}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>
  • ${#fields} 로 BindingResult 가 제공하는 검증 오류에 접근할 수 있다.
    - ${#fields.hasGlobalErrors()} : 글로벌 에러가 존재하는지 확인하는 메서드로 있을 때 true를 반환한다.
    • ${#fields.globalErrors()} : 글로벌 에러에 접근한다.
  • th:errors는 필드에 에러가 있을 경우에 태그를 렌더링한다.
  • th:errorclass는 필드에 에러가 있으면 class를 추가한다.

BindingResult를 이용하면 @ModelAttribute 데이터 바인딩으로 인한 에러가 발생해도 컨트롤러가 호출된다.

  • 확인을 해보면 에러 메세지가 정상적으로 뜨고 컨트롤러도 정상적으로 호출되는 것을 알 수 있다!!!

  • 그런데 데이터 타입은 같은데 필드에러가 발생한다면?

    가격에 100을 입력하여 필드 에러가 발생하니 값이 유지되지 않는다. 입력 값이 유지되도록 해보자.

컨트롤러 수정

    @PostMapping("/add")
    public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        // 검증 로직
        // 상품명이 없을 때
        if (!StringUtils.hasText(item.getItemName())) {
            // ctrl + p : 필요한 파라미터를 알려줌
            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) {
                // 특정 필드 에러가 아닌 글로벌 에러이므로 ObjectError로 담아줌
                bindingResult.addError(new ObjectError("item",null, null, "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
            }
        }
        // 생략
    }
  • 앞서 사용했던 FieldError 생성자와는 파라미터 값이 다른 것을 확인할 수 있다.
    FieldError는 2개의 생성자를 지원하는데, 또 다른 생성자에 필요한 파라미터는 아래와 같다.
    objectName : 오류가 발생한 객체 이름
    field : 오류 필드
    rejectedValue : 사용자가 입력한 값(거절된 값)
    bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값(boolean)
    codes : 메시지 코드
    arguments : 메시지에서 사용하는 인자
    defaultMessage : 기본 오류 메시지
  • bindingFailure가 데이터 타입으로 인한 에러인지 확인하는 파라미터인데, 여기서는 타입으로 인한 에러는 아니므로 false로 설정했다.

view 수정?

  • view는 따로 수정할 필요가 없는데, 그 이유는 th:field 때문이다. th:field는 필드의 id, name, value를 생성해주는데, 정상적인 상황에서는 객체의 값을 이용하지만, 에러 발생시에는 FieldError에 보관된 값(rejectedValue)을 출력한다.
  • 이제 다시 확인을 해보자!!!

    업로드중..
  • 바인딩 에러든, 검증 실패든지 잘 동작하고 값을 유지하는 것을 확인할 수 있다!!!
    그런데 에러 메세지가 너무 개발자스러운 문제가 있다. 다음 포스팅에서는 이것을 개선해보자

출처 : 김영한님의 스프링MVC 2편

profile
꾸준히 하자!

0개의 댓글