Controller Validation

wangjh789·2022년 8월 8일
0

[Spring] 스프링-mvc-2

목록 보기
2/11

BindingResult - 1

원래 @ModelAttribute의 바인딩 시점에 타입 오류가 발생하면 400에러를 뱉는다.
하지만 BindingResult가 있으면 오류가 발생해도 컨트롤러가 호출된다.

타입에러 발생시 Spring이 자동으로 typeMisMatch 라는 FieldError를 담는다.
BindingResult 객체는 자동으로 Model에 담겨서 뷰에 전달된다.

	@PostMapping("/add")
    public String addItem(@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","상품 가격은 천원 ~ 백만원 사이를 허용합니다."));
        }
        if (item.getQuantity() == null || item.getQuantity() >= 9999) {
            bindingResult.addError(new FieldError("item","quantity","수량은 최대 9999까지 허용합니다."));
        }
        //특정 필드가 아닌 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int result = item.getPrice() * item.getQuantity();
            if (result < 10000) {
                bindingResult.addError(new ObjectError("item","가격 * 수량의 합은 10000원 이상이어야 합니다."));
            }
        }

        //검증에 실패하면 다시 입력 폼으로
        if (bindingResult.hasErrors()) {
            return "validation/v2/addForm";
        }

        //성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v2/items/{itemId}";
    }

FieldError : itemName == Null, price < 1000 같이 필드 하나로 구성된 에러
ObjectError : quantity * price < 100000 같은 두 가지 이상 복합 필드로 이루어진 에러

타임리프

스프링부트의 뷰템플릿을 타임리프로 사용하면 다음과 같이 검증 부분의 편의성을 제공한다.

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

th:field 는 해당 필드의 에러 유무를 알고 있다.
정상 상황에선 모델 객체의 값을 사용하지만, 에러 발생 시 FieldError의 RejectedValue를 사용하고th:errorclass의 class를 붙여준다.
th:errors는 에러가 있을 시에만 보여지고 에러내용을 출력한다.

에러가 발생했을 때 사용자가 입력한 값을 그대로 유지하려면 어떻게 해야할까?
-> FieldError의 두번째 생성자 rejectedValue를 설정한다.
ArgumentResolver가 HttpResolver를 호출해 Item 객체를 생성할 때 발생한 에러를 BindingResult에 담아 컨트롤러에 넘겨준다.

BindingResult - 2

        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() >= 9999) {
            bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]{9999}, null));
        }
        //특정 필드가 아닌 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int result = item.getPrice() * item.getQuantity();
            if (result < 10000) bindingResult.reject("totalPriceMin",new Object[]{10000,result},null);
        }

ObjectError와 FieldError 객체를 새로 생성하는 것 대신 reject(), rejectValue()로 대체할 수 있다.
이는 BindingResult가 objectName(=item)과 target(=Item)을 알고 있기 때문에 생략한 버전이다.

실행순서

  • rejectValue() 호출
  • MessageCodesResolver가 검증 오류 코드로 메시지 코드들을 생성
  • new FieldError()를 생성해 메시지 코드들을 보관
  • th:errors 에서 메시지를 순서대로 찾고, 노출

축약된 오류코드
FieldError()를 직접 다룰 때는 range.item.price 와 같이 오류코드를 모두 입력했다.
rejectedValue()를 사용한 후 range와 같이 축약된 오류코드를 입력했는데 이는 MessageCodesResolver가 BindingResult가 알고있는 objectName, target을 조합해 알맞은 MessageSource 내에서 메시지 코드들을 생성하기에 가능하다.

th:errors가 메시지를 찾는 순위는 세부적인 레벨에서 대략적인 레벨 순이다.(errorCode + objectName + field)
range.item.price -> range.item -> range

스프링이 만든 오류코드

오류코드는 2가지 종류가 있다.

  • 사용자가 reject()로 직접 생성한 오류코드 (required)
  • 스프링이 자동으로 생성한 오류코드 (typeMismatch)
    Integer 타입의 필드에 다른 타입이 입력되는 경우를 위해 스프링은 typeMismatch 메시지를 만들어두었다.
    Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Integer' for property 'price'; nested exception is java.lang.NumberFormatException: For input string: "ㅂ"
    이 경우 MessageCodesResolver는 아래와 같은 메시지 코드를 생성한다.
    typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch
    클라이언트에게 불친절한 메시지를 보여줄 수 없기에 새로운 메시지로 덮어씌워야 한다.
profile
기록

0개의 댓글