오봉이와 함께하는 개발 블로그
스프링 MVC 2 - 검증 직접 처리 본문
검증 직접 처리 - 소개
성공
사용자가 상품 등록 폼에서 정상적인 범위의 데이터를 입력하면, 서버에서는 검증 로직이 통과하고, 상품을 저장 후 상품 상세 화면으로 redirect 한다.
실패
고객이 검증 범위를 넘어서면 서버 검증 로직이 실패해야 한다.
이렇게 검증이 실패한 경우 고객에게 다시 상품 등록 폼을 보여주고, 고객이 입력헀던 값을 다시 model에 담아 전달해서 값을 다시 입력해놔야 한다. 어떤 값을 잘못 입력했는지 알려주어야 한다.
검증 직접 처리 - 개발
상품 등록 검증 코드를 개발하자.
ValidationItemControllerV1 - addItem() 수정
@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()) {
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<String, String> errors = new HashMap<>();
검증 오루 발생시 어떤 검증에서 오류가 발생했는지 정보를 담을 객체
검증이 실패하면 다시 입력 폼으로
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
해당 로직을 적용하고 실행해서 검증에 실패했을 때 return "validation/v1/addForm";
를 다시 요청하는데 전에 입력했던 모든 데이터가 그대로 남아있다.
@ModelAttribute Item item
가 있어서 그렇다.
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v1/addForm";
}
위 코드를 통해 Item 객체를 만들어서 넘겨주고, 사용자가 입력 후 검증에 실패 했다.
그럼 return "validation/v1/addForm";
를 실행하게 되고 다시 입력 폼으로 이동하는데, @ModelAttribute Item item
가 자동으로 model.addAttribute("item", item)
을 호출해서 입력했던 값 그대로 다시 넘어가게 된다.
addForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 th:text="#{page.addItem}">상품 등록</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 메시지</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
placeholder="이름을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
상품명 오류
</div>
</div>
<div>
<label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price" th:field="*{price}" class="form-control" th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control'"
placeholder="가격을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('price')}" th:text="${errors['price']}">
가격 오류
</div>
</div>
<div>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:field="*{quantity}" class="form-control" th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'"
placeholder="수량을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${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/v1/items}'|"
type="button" th:text="#{button.cancel}">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
글로벌 오류 메시지
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
오류 메시지는 errors
에 내용이 있을 때만 출력하면 된다.th:if="${errors?.containsKey('globalError')}"
를 통해 errors
에 key값으로 globalError
가 있는지 검사한다.
있으면 th:text="${errors['globalError']}"
를 통해 value에 접근해서 값을 출력한다.
참고 Safe Navigation Operator
만약 errors
가 null
이라면?
처음 등록 폼에 진입한 시점에는 errors
가 없기 때문에 errors.containsKey()
를 호출하는 순간 NullPointerException
가 발생한다.
errors?.
는 errors
가 null
일 때 NPE
이 발생하는 대신 null
을 반환하는 문법이다.th:if
에서 null
은 실패로 처리되므로 오류 메시지가 출력되지 않는다.
SprinEL이 제공하는 문법이다.
필드 오류 처리
<!-- v1 -->
<input type="text" id="quantity" th:field="*{quantity}" class="form-control" th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'" placeholder="수량을 입력하세요">
<!-- v2(개선) -->
<input type="text" th:classappend="${errors?.containsKey('itemName')} ? 'field-error' : _" class="form-control">
v1
은 클래스 이름을 바꾸는 코드고, v2
는 classappend
를 사용해서 앞에 일므을 추가하는 코드다.
만약 값이 없으면 _
(No-Operation)을 사용해서 아무것도 하지 않는다.
필드 오류 처리 - 메시지
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
상품명 오류
</div>
글로벌 오류 메시지와 동일하다.
정리
- 만약 검증 오류가 발생하면 입력 폼을 다시 보여준다.
- 검증 오류들을 고객에게 친절하게 안내해서 다시 입력할 수 있게 한다.
- 검증 오류가 발생해도 고객이 입력한 데이터가 유지된다.
남은 문제
- 뷰 템플릿에서 중복 처리가 많다.
- 코드들이 다 비슷하다
- 타입 오류 처리가 안 된다.
- price, quantity같은 숫자 필드는 타입이 Integer이므로 문자 타입으로 설정하는 것이 불가능하다.
- 숫자 타입에 문자가 들어오면 오류가 발생하지만, 스프링 MVC에서 컨트롤러에 진입하기도 전에 예외가 발생하기 때문에 컨트롤러 호출도 안 되며 400 에러가 떨어지고 오류 페이지를 띄운다.
- Item의 price에 문자를 입력하는 것 처럼 타입 오류가 발생해도 고객이 입력한 문자를 화면에 남겨야 한다.
- 만약 컨트롤러가 호출된다고 가정해도 Item의 price는 Integer이므로 문자를 보관할 수가 없다.
- 결국 문자 바인딩이 불가능하므로 입력한 문자가 사라지고, 어떤 내용을 입력해서 오류가 발생했는지 이해하기 어렵다.
- 결국 입력한 값도 어딘가에 별도로 관리되어야 한다.
출처 : 인프런 김영한 지식공유자님 강의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술
'BE > Spring' 카테고리의 다른 글
스프링 MVC 2 - FieldError, ObjectError (0) | 2022.08.23 |
---|---|
스프링 MVC 2 - BindingResult (0) | 2022.08.23 |
스프링 MVC 2 - 검증(Validation) 요구사항 (0) | 2022.08.18 |
스프링 MVC 2 - 스프링 메시지 소스 사용 (0) | 2022.08.18 |
스프링 MVC 2 - 스프링 메시지 소스 설정 (0) | 2022.08.18 |