사용자가 어플리케이션을 사용하며 어떠한 입력하는 동작에서 해당 값이 유효한 값인지 검사하는 것이다.
//검증 오류 결과를 보관
Map<String,String> errors = new HashMap<>();
//검증 로직
if(!StringUtils.hasText(item.getItemName())){
errors.put("itemName","상품 이름은 필수입니다.");
}
if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice()>1000000){
errors.put("price", "가격은 1,000원 ~ 1,000,000 까지 허용합니다.");
}
if(item.getQuantity() == null || item.getQuantity() >= 9999){
errors.put("quantity", "수량은 최대 9,999 까지 허용합니다.");
}
//특정 필드가 아닌 복합 룰 검증
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
errors.put("golbalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = "+resultPrice);
}
}
//검증 실패시 다시 form 으로
if(!errors.isEmpty()){ //부정의 부정은 코드의 가독성을 떨어트린다.
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
//검증에 성공
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
검증을 직접 개발한다면 Map에 검증에 실패한 내용을 담아서 model로 넘겨야한다. 그리고 검증에 실패했을때 연결되는 페이지 또한 등록하는 페이지로 연결해야한다.
여기서 중요한 점은 form에서 넘기는 객체의 정보를 검증하는 로직에서도 가져가야한다는 것이다. model에 계속 객체를 가지고 있어야 만약 검증에 실패했을 때 보여지는 addForm에서 사용자가 입력했던 값들을 그대로 가지고 다시 출력해줄 수 있어야하기 때문이다.
실제로 자신의 템플릿 엔진에 맞게 설정하여 errors의 값이 존재할때 다음과 같이 출력해주면 된다.
위 같이 처리할때의 문제점
@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){
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = "+resultPrice));
}
}
//검증 실패시 다시 form 으로
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}";
}
먼저 코드를 설명하자면 기존 위에 있던 코드는 @ModelAttribute에 Item을 가져와 Map에 오류를 key로 담고 메세지를 value로 담아서 반환했다면 위 코드는 스프링에서 제공하는 BindingResult를 사용하는 코드이다. 여기서 중요한 점은 BindingResult는 무조건
@ModelAtrribute의 뒤에 있어야 정상적으로 작동한다는 것이다. 그리고 front의 문법 또한 변환되므로 문법도 수정해주어야한다. 나의 예시는 타임리프 뿐이라 타임리프의 코드만 적어놓겠다. 그리고 BindingResult를 처음 보거나 front 문법이 이해가 안된다고 해서 걱정할 필요가 없다. 우리가 실무에서 사용하는 검증 방법은 아니니깐! (물론 사용하는 곳도 있겠지만)
<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>
위와 같이 작성 시 정상적으로 작동하는 것을 확인할 수 있다.