1. 코드 개선을 위한 코드 복사 및 변경
코드 replace
검증을 위한 코드를 개선해본다. 우선 앞선 글에서 작성한 코드를 복사해서 V2 버전으로 만든다. 코드 내에 있는 v1, V1 부분을 v2, V2로 만드는데, 변경을 원하는 코드를 블록하고, [Ctrl + R]을 누르면 intelliJ에서 선택하여 변경, replace All 기능을 제공한다.
디렉토리 내 단어 변경
[Ctrl + Shift + R]을 누르면 디렉토리 내부의 단어들을 전부 바꿔줄 수 있다.
2. BindingResult 적용 : 기본
에러의 종류와 메시지를 담는 errors 객체를 직접 생성하는 것이 아니라, 스프링에서 제공하는 BindingResult 객체를 사용하도록 한다. BindingResult 객체는 View로 넘기는 model에 담는 item 객체를 자동으로 View로 넘겨준다. BindingResult 객체 자체가 Model에 담겨서 View로 전달된다. 그리고 여러가지 편의 기능을 제공한다.
컨트롤러 처리
BindingResult 파라미터 추가
컨트롤러의 파라미터 목록에 BindingResult를 추가하였다. 이때 반드시 bindingResult를 통해 View로 넘기고자 하는 객체 바로 뒤에 BindingResult 파라미터를 추가해야 한다.
addError() : FieldError, ObjectError
검증 로직 조건문에 걸리면, addError를 통해 View로 넘겨줄 bindingResult안에 에러 메시지들을 담을 수 있다. 특정 필드와 관련된 에러는 FieldError("객체 이름", "필드 이름", "에러 메시지")로 넘기고, 전체와 관련 있는 에러는 ObjectError("객체 이름", "에러 메시지")로 작성해주면 된다.
hasErrors()
bindingResult에 에러가 있는지 검사하는 메서드는 hasErrors()이다. 앞에서 언급한 대로, bindingResult는 model에 errors 객체를 담는 작업을 자동으로 처리해주므로, 따로 model 객체에 bindingResult를 담을 필요가 없다.
java/hello/itemservice/web/validation/ValidationItemControllerV2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@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() >= 10000) {
bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9,999까지 허용합니다."));
}
if (item.getPrice() != null && item.getQuantity() != null) {
int totalPrice = item.getQuantity() * item.getPrice();
if (totalPrice < 10000) {
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이여야 합니다. 현재 값 = " + totalPrice));
}
}
log.info("bindingResult ={}", bindingResult);
//검증 로직 실패 시, 다시 입력 폼으로
//이중 부정이므로 리팩토링으로 hasError 등으로 바꾸는게 좋다.
if (bindingResult.hasErrors()) {
return "validation/v2/addForm";
}
//검증 로직 통과 시, 상품 저장
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
}
|
cs |
View 처리
bindingResult를 받을 수 있도록 View 코드도 수정한다.
글로벌 에러 -> th:if = "${#fields.hasGlobalErrors()}" 적용
글로벌 에러의 경우 th:if 문을 쓰되, #fields.hasGlobalErrors() 문법을 적용한다. 그리고 여러 개가 있을 지 모르는 에러에 대해서 th:each 문으로 다시 #fields.globalErrors()문법을 사용하면 각 global 에러에 해당하는 에러메시지를 출력하는 요소를 만들 수 있다.
각 필드 에러 -> th:errors="*{필드명}"
에러가 있을 때만 연산이 작동하여 에러를 띄워주는 코드이다. 상위 input 태그의 th:field를 참조하여 필드명을 알아내는 방식이기 때문에, $가 아니라 참조 문법인 *을 사용했음에 유의하자.
에러 시 클래스 추가 -> th:errorclass
필드 에러 발생 시 클래스명을 추가해주는 문법이다.
상품명, 가격, 수량 모두 같은 문법을 적용한다. 수량 부분에 주석처리된 부분은 v1에서 적용하던 부분이다. 조건에 대한 분기처리를 자동으로 해준다는 것을 알 수 있다.
resources/templates/validation/v2/addForm.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<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="수량을 입력하세요">
<!-- th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'"-->
<!-- <div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">수량 오류</div>-->
<div class="field-error" th:errors="*{quantity}">수량 오류</div>
</div>
|
cs |
3. BindingResult 적용 : 상세
BindingResult 세부 내용
스프링은 오류가 있으면 오류 정보를 BindingResult에 담고, 컨트롤러를 정상 호출한다.
스프링은 @ModelAttribute가 적용된 객체에 오류 등으로 인해 에러가 발생한 경우에 FieldError를 생성하여 BindingResult에 넣어준다. 컨트롤러에 BindingResult 파라미터가 포함된채로, 상품 등록 시 가격에 숫자가 아니라 문자값을 입력해보면, BindingResult가 에러를 처리하여 View에서 에러 정보가 보이는 것을 볼 수 있다.
이 내용을 위 사진의 예로 설명하자면, 원래 컨트롤러가 정상 호출되어 item 객체의 price 필드에 "ㅁㅁㅁ" 값이 담겨서 View로 전달되어야 하는데 price 필드는 Integer 타입이므로 문자열인 "ㅁㅁㅁ"를 담지 못하게 되고, 컨틀롤러 자체가 정상호출되지 못해서 400 에러가 발생하며 WhiteLabel Error Page가 호출된 것이다.
※ BindingResult는 인터페이스이며, Errors 인터페이스를 상속 받는다. 그러나 Errors 인터페이스는 addError()와 같은 편의 메서드가 적기 때문에, 보통 BindingResult를 많이 사용한다.
FieldError, ObjectError 세부 내용
지정한 에러가 발생하면 입력값이 사라진다.
만약 가격 필드만 에러가 발생하도록 10을 입력하면, 가격 필드에 입력한 정보는 사라진다. 이것은 가격 필드에서 발생한 FieldError 객체에 사용자의 입력 정보가 담기지 못한채로 View로 넘어갔기 때문이다.
rejectedValue, bindingFailure
에러가 발생한 사용자의 입력값을 담아주는 파라미터는 rejectedValue라는 이름으로 저장되게 할 수 있다. 컨트롤러에서 addFormV1 코드를 복사해서 addFormV2로 바꾸고, FieldError와 ObjectError에 이 파라미터를 추가해보자.
FieldError는 생성자의 종류가 2개이다. 그 중 아래쪽의 생성자를 이용하여 rejectedValue를 추가할 것이다. 어떤 객체의 파라미터를 알아보는 단축키는 [Ctrl + P] 이다.
FieldError에는 rejectedValue, bindingFailure, codes, arguments 및 defaultMessage를 파라미터로 추가하였다. bindingFailure는 사용자가 값 자체를 입력안했는지 물어보는 Boolean 값이다. 그리고 codes, arguments는 다음 글에서 소개한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
// bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000원 ~ 1,000,000 까지 허용합니다."));
}
if (item.getQuantity() == null || item.getQuantity() >= 10000) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, null, null, "수량은 최대 9,999까지 허용합니다."));
}
if (item.getPrice() != null && item.getQuantity() != null) {
int totalPrice = item.getQuantity() * item.getPrice();
if (totalPrice < 10000) {
bindingResult.addError(new ObjectError("item", null, null, "가격 * 수량의 합은 10,000원 이상이여야 합니다. 현재 값 = " + totalPrice));
}
}
|
cs |
이렇게 적용하면 에러가 발생해도 사용자의 입력값이 저장되는 것을 확인할 수 있다.
참조
1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC-2] 8. 검증(Validation) - Bean Validation 사용하기 (0) | 2021.08.24 |
---|---|
[스프링 웹MVC-2] 7. 검증(Validation) - 오류코드, 메시지 처리 (0) | 2021.08.22 |
[스프링 웹MVC-2] 5. 검증(Validation) (0) | 2021.08.21 |
[스프링 웹MVC-2] 4. 메시지, 국제화 (0) | 2021.08.20 |
[스프링 웹MVC-2] 3. 타임리프/스프링으로 폼 양식 만들기 - th:object, th:field, 체크박스, 라디오 버튼, 셀렉트 박스 (0) | 2021.08.16 |