내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
1. 검증 직접 처리 - 소개
컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
그리고 정상 로직보다 이런 검증 로직을 잘 개발하는 것이 어쩌면 더 어려울 수 있다.
우선 상품 저장이 검증 실패되는 경우를 그림으로 살펴봅시다.
고객이 폼에 입력한 값이 지정된 범위를 넘어가거나, 또는 너무 작은경우 서버에서 검증 로직이 실패해야 한다.
이렇게 검증에 실패한 경우 고객이 보냈던 실패한 데이터를 Model에 저장하여 다시 반환함으로써 어떤 값을 잘못 입력했는지를 알려줘야 한다
만약 검증이 실패한 경우에는 다시 상품 등록 폼을 보여줘야 한다. 다만 이전에 실패한 데이터가 그대로 함께 보여줘야한다.
2. 검증 직접 처리 - 개발
우선 상품등록 컨트롤러에 검증 로직을 직접 구현해 봅시다.
@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원 이상이어야 합니다.");
}
}
// 검증에 실패한 경우
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을 사용하였습니다. 발생한 오류의 정보를 담아둡니다.
Map<String, String> errors = new HashMap<>();
검증시 오류가 발생하면 errors 에 담아둔다. 이때 어떤 필드에서 오류가 발생했는지 구분하기 위해 오류가 발생한 필드명을 key 로 사용한다. 이후에 뷰를 작성할때 Model로부터 넘겨 받은 errors Map을 통하여 key값을 통해 해당 에러가 있는지 확인후 메시지를 출력한다.
● 특정 필드의 범위를 넘어서는 검증 로직
요구 조건중에서 가격 * 수량이 10000 이상이어야 한다는 조건이 있었다.
이는 특정 필드에 국한되는 에러가 아니다. 이때는 필드 이름을 넣을 수 없으므로 globalError 라는 key 를 사용한다.
//특정 필드의 범위를 넘어서는 검증 로직
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
}
}
● 검증에 실패한 경우
검증에 실패하게된 경우 오류 메시지가 errors 라는 Map에 저장되게 된다. 이 Map을 Model에 담아서 입력 폼이있는 뷰 로 넘기면 된다.
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
오류가 난 경우 상용자가 잘못 입력한 데이터를 그대로 갖고 처음봤던 Form으로 돌아가는 것 이다.
한가지 의문이 든다? 잘못 입력한 데이터는 어떻게 우리의 화면에 다시 출력이 될까?
처음 등록 Form을 보여주는 컨트롤러는 다음과 같다.
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v1/addForm";
}
해당 컨트롤러에서는 Item 이라는 텅빈 객체를 만들어서 Model에 등록했다.
이를 뷰에서 사용자가 폼에 입력했을때 해당 Item 객체에 값을 바인딩 한다.
이후 폼을 제출하게 되면 PostMapping을 하는 컨트롤러에게 이 Item 객체가 전달된다.
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
// 검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
// 생략...
}
이때 @ModelAttribute 를 통해 자동으로 Item이 Model에 등록된다.
@ModelAttribute 는 인자로 받은 item객체를 자동으로 Model에 class이름의 첫글자를 소문자로 바꾸어 등록해 준다.
따라서 다시 뷰로 이동하게 된 경우, 이전까지는 빈 객체였던 Item에 이번에는 데이터가 들어있어 이를 보여주게 되는것 이다.
즉, Form에서 전달한 데이터를 갖은 Item객체가 그대로 다시 Model에 등록되어 뷰로 전달되는 것 이다.
● Form에서 오류 메시지 보여주기
하지만 오류를 발생시키기 위해 입력을 하지 않고 폼을 제출해도 어떠한 경고도 보이지 않는다.
이는 우리가 뷰에서 에러가 발생한경우의 처리를 해주지 않았기 때문이다. 따라서 뷰를 다음과 같이 수정해야 한다.
<!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}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
class="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}"
th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control'"
class="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}"
th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'"
class="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>
대표적으로 상품명에 관한부분을 살펴보자.
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
상품명 오류
</div>
● 필드 오류 처리 - 메시지
상품명 오류 부분에 th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}" 를 살펴보면,
(errors?의 ?는 밑에서 따로 살펴보자)
Model로 넘긴 errors가 있는지 확인한 후, 있다면 'itemName'이라는 key가 있는지 확인한다.
해당 키가 존재하기 때문에 th:if문이 true가 되고 th:text를 통하여 errors에서 value를 추출하여 보여준다.
● 필드 오류 처리 - 입력 폼 색상 적용
해당 Form 입력 칸에 빨간 태두리를 만들기 위해 다음과 같이 class도 변경하였다.
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
3항 연산자 라고 생각하면 편한 내용이다. 에러가 발생한다면 다음과 같이 랜더링 된다.
<input type="text" class="form-control field-error">
결과는 다음과 같이 보이게 된다.
오류가 난 칸에 빨간색의 태두리를 확인할 수 있다.
● Safe Navigation Operator
위에서 errors?를 본적이 있다. ?가 없는 errors에 null 이 전달된다면 어떻게 될까?
생각해보면 등록폼에 진입한 시점에는 errors 가 없다. 따라서 errors.containsKey() 를 호출하는 순간 NullPointerException 이 발생한다.
따러서 ?를 붙인 errors?. 는 errors가 null 일때 NullPointerException 이 발생하지 않고, null 을 반환하는 문법이다.
th:if 에서 null 은 실패로 처리되므로 오류 메시지가 출력되지 않는다.
● 남은 문제점들
타입 오류 처리가 안된다. Item 의 price , quantity 같은 숫자 필드는 타입이 Integer 이므로 문자 타입으로 설정하는 것이 불가능하다.
숫자 타입에 문자가 들어오면 오류가 발생한다.
그런데 이러한 오류는 스프링MVC에서 컨트롤러에 진입하기도 전에 예외가 발생하기 때문에, 컨트롤러가 호출되지도 않고, 400 예외가 발생하면서 오류 페이지를 띄워준다.
이를 스프링을 통해 해결해 나가보자.
인텔리제이 replace 단축키 => cmd + R
폴더 내부에 들어있는 파일 전부 replace 하기 => cmd + shift + R
'BackEnd > Spring MVC' 카테고리의 다른 글
[Spring] 검증1 - Validation - 3 (0) | 2022.03.06 |
---|---|
[Spring] 검증1 - Validation - 2 (0) | 2022.03.06 |
[Spring] 메시지, 국제화 (0) | 2022.03.05 |
[Spring] 스프링 MVC - 기본 기능 - 3 (0) | 2022.02.28 |
[Spring] 스프링 MVC - 기본 기능 - 2 (0) | 2022.02.27 |
댓글