원래 @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에 담아 컨트롤러에 넘겨준다.
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)을 알고 있기 때문에 생략한 버전이다.
실행순서
축약된 오류코드
FieldError()를 직접 다룰 때는 range.item.price 와 같이 오류코드를 모두 입력했다.
rejectedValue()를 사용한 후 range와 같이 축약된 오류코드를 입력했는데 이는 MessageCodesResolver가 BindingResult가 알고있는 objectName, target을 조합해 알맞은 MessageSource 내에서 메시지 코드들을 생성하기에 가능하다.
th:errors가 메시지를 찾는 순위는 세부적인 레벨에서 대략적인 레벨 순이다.(errorCode + objectName + field)
range.item.price -> range.item -> range
오류코드는 2가지 종류가 있다.
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: "ㅂ"
typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch