검증 - Validation
입력 타입이 잘못되어서 오류가 나는 경우를 처리 해주어야한다.
잘못된 입력으로 검증 오류가 발생하면 오류 화면으로 바로 이동하게 되면, 고객 입장에서는 입력 해놓은 것들이 날라가서 다시 입력하는 것이 비효율적이어서 고객이탈의 원인이 될 수 있다.
따라서 웹 서비스는 폼 입력시 오류가 발생하면, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 친절하게 알려주어야 한다.
컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함
서버의 검증 로직이 실패한 경우 고객에게 다시 상품 등록 폼과 기존에 입력된 값을 전달하고 어떤 값을 잘못 입력했는지 알려주어야 한다.
상품입력을 예로 들어보면 아래와 같은 로직으로 구성할 수 있다. 잘못된 입력이 들어오면 다시 등록폼을 리다이렉트 시켜서 입력을 그대로 넘겨주고 입력을 다시 하도록 한다.
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
//검증 오류 결과를 보관
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("globalError","가격 * 수량의 합은 10,000원 이사이어야 합니다. 현재 값 =" + resultPrice);
}
}
//검증에 실패하면 다시 입력 폼으로
if(!errors.isEmpty()){
log.info("errors={}",errors);
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}";
}
기존에 비어있는 Item객체를 넘겨줬던 이유는 th:object를 사용하기 위해서였다. 추가적으로 이것 말고도 검증에 실패했을 때도 넘어간 데이터가 다시 보이도록 재사용하는데도 사용된다.
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v1/addForm";
}
검증을 통해서 잘못된 입력을 표시해주기 위해 css를 추가했다.
.field-error {
border-color: #dc3545;
color: #dc3545;
}
글로벌 오류 메시지
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
오류 메시지는 errors 에 내용이 있을 때만 출력하면 된다. 타임리프의 th:if 를 사용하면 조건에 만족할 때만 해당 HTML 태그를 출력할 수 있다.
만약 여기에서 errors 가 null 이라면 어떻게 될까?
생각해보면 등록폼에 진입한 시점에는 errors 가 없다.
따라서 errors.containsKey() 를 호출하는 순간 NullPointerException 이 발생한다.
errors?. 은 errors 가 null 일때 NullPointerException 이 발생하는 대신, null 을 반환하는 문법이다.
th:if 에서 null 은 실패로 처리되므로 오류 메시지가 출력되지 않는다.
필드 오류 처리 - 메시지
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
상품명 오류
</div>
필드 오류 처리 - 입력 폼 색상 적용
<input type="text" class="form-control field-error">

만약 검증 오류가 발생하면 입력 폼을 다시 보여준다.
검증 오류들을 고객에게 친절하게 안내해서 다시 입력할 수 있게 한다.
검증 오류가 발생해도 고객이 입력한 데이터가 유지된다.