내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
3. BindingResult 1
스프링이 제공하는 BindingResult를 통해 검증 오류 처리 방법을 알아보자.
● addItemV1
우선 이전 글에서 하나하나 검증하던 코드가 어떻게 변경되었는지 확인해 보자.
@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원 이상이어야 합니다."));
}
}
// 검증에 실패한 경우
if(bindingResult.hasErrors()){
log.info("errors= {}", bindingResult);
return "validation/v2/addForm";
}
// 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
우선 addItemV1 컨트롤러의 인자로 BindingResult 가 추가되었다. 이전에 오류를 저장해두는 errors Map과같은 역할을 한다.
BindingResult bindingResult 파라미터의 위치는 @ModelAttribute Item item 다음에 와야 한다.
- 필드 오류 : FieldError
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
필드에 오류가 있는 경우, 이전과 달리 FieldError이라는 객체를 만들어 bindingResult에 저장한다.
인자로는 객체이름(@ModelAttribute 이름), 필드이름, 오류메시지 를 넘긴다. 생성자는 다음과 같은 형태를 갖기 때문이다.
public FieldError(String objectName, String field, String defaultMessage) {}
(ps, 함수의 파라미터 확인하는 단축키 => cmd + P)
- 글로벌 오류 : ObjectError
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상 이어야 합니다. 현재 값 = " + resultPrice));
특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서 bindingResult 에 담아두면 된다.
● addForm.html 수정
<form action="item.html" th:action th:object="${item}" method="post">
<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>
// 생략
</form>
타임리프는 스프링의 BindingResult를 활용한 검증오류를 표현하는 편리한 기능들을 제공한다.
- #fields : 타임리프가 제공하는 기본 객체이다. #fields로 BindingResult가 제공하는 검증 오류에 접근할 수 있다.
- th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전이다.
- th:errorclass : th:field 에서 지정한 필드 이름으로 BindfingResult에 오류가 있으면 class 정보를 추가한다.
- 글로벌 오류 처리
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
</div>
글로벌 오류같은 경우 하나가 아닐수가 있다. 여러 글로벌 에러가 담겨있을수도 있다.
따라서 th:each 반복문을 통하여 출력하도록 하면, 오류가 여러개일 경우 <p></p> 태그를 여러번 사용하여 모두 출력하게 된다.
- 필드 오류 처리
<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을입력하세요">
<div class="field-error" th:errors="*{itemName}">
상품명 오류
</div>
itemName field상에 오류가 있음을 인식하면 th:errorclass의 내용을 기존의 class에 추가시켜 준다.
그럼 th:errorclass 에서는 필드명을 지정하지 않았는데 어떻게 필드에 오류가 발생했는지 알수있을까?
=> th:field="*{itemName}" 덕분이다!
"field-error" 가 빨간 태두리를 만들어주는 class이기 때문에 오류가 발생하면 해당 class가 추가되어 빨간 태두리가 생기게 된다.
th:errors="*{itemName}" 는 itemName 필드에 해당하는 오류가 있다면 해당 <div>상품명 오류</div>태그를 출력해 준다.
기존의 th:if문으로 확인하던 코드에 비하면 확 줄어들었다.
기존 코드
th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}"
4. BindingResult 2
BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체이다. 검증 오류가 발생하면 여기에 보관하면 된다.
BindingResult 가 있으면 @ModelAttribute 에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다!
● @ModelAttribute에 바인딩 시 타입 오류가 발생하면 어떻게 될까?
- BindingResult 가 없으면, 400 오류가 발생하면서 컨트롤러가 호출되지 않고, 스프링이 보여주는 오류 페이지로 이동한다.
- BindingResult 가 있으면, 오류 정보( FieldError )를 BindingResult 에 담아서 컨트롤러를 정상 호출한다.
● BindingResult 로 오류를 검증하는 3가지 방법
1) @ModelAttribute의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 FieldError를 생성해서 BindingResult 에 넣어준다.(스프링이 바인딩 오류가 발생시 추가해주는)
2) 개발자가 검증 로직을 구현하여 직접 추가해 준다. (비지니스 로직에서의 검증에서 추가)
3) Validator를 사용한다.
(주의! 위에서도 언급했지만, @ModelAttribute Item item 바로 다음에 BindingResult 가 와야 한다.)
숫자가 입력되야하는 칸에 문자를 입력하여 오류를 확인해 보자.
위와같이 int를 받는 가격부분에 "문자입력" 이라는 String을 전달하였다.
결과는 다음과 같다.
총 2가지 error가 확인 가능하다.
1) Field error가 'item'이라는 객체에서 발생했다고 나온다. price에는 [문자입력] 이라는 잘못 입력된 값 이 저장되어있다.
이는 Spring이 @ModelAttribute의 객체에 타입 오류 등으로 바인딩이 실패하였기에 자동으로 추가해준 것 이다.
2) 두번째로 추가된 Field error는 rejected value가 null로 되어있는데, 이는 우리가 직접 구현한 검증 로직에서 추가된 것 이다.
bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
● BindingResult와 Errors
BindingResult 는 인터페이스이고, Errors 인터페이스를 상속받고 있다.
실제 넘어오는 구현체는 BeanPropertyBindingResult 라는 것인데, 둘다 구현하고 있으므로 BindingResult 대신에 Errors 를 사용해도 된다.
하지만 실제로 Errors로 변경하면 코드에서 오류가 발생하는데, 이는 Errors에는 없는 BindingResult 만의 메서드를 사용했기 때문이다.
BindingResult , FieldError , ObjectError 를 사용해서 오류 메시지를 처리하는 방법을 알아보았다. 남은 문제가 있는데...
오류가 발생하는 경우 고객이 입력한 내용이 모두 사라진다. 이 문제를 해결해보자.
5. FieldError, ObjectError
● addItemV2
@PostMapping("/add")
public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
// 검증 로직
if(!StringUtils.hasText(item.getItemName())){
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() >= 9999){
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, null, null, "수량은 최대 9,999 까지 허용합니다."));
}
// 복합 룰 검증
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
bindingResult.addError(new ObjectError("item", null, null,"가격 * 수량의 합은 10,000원 이상이어야 합니다."));
}
}
// 검증에 실패한 경우
if(bindingResult.hasErrors()){
log.info("errors= {}", bindingResult);
return "validation/v2/addForm";
}
// 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
● FieldError 생성자
FieldError 는 두 가지 생성자를 제공한다. 위 컨트롤러에서도 변경된 FieldError를 사용하였다.
public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object rejectedValue,
boolean bindingFailure, @Nullable String[] codes, @Nullable Object[] arguments,
@Nullable String defaultMessage)
- 파라미터 목록
objectName : 오류가 발생한 객체 이름
field : 오류 필드
rejectedValue : 사용자가 입력한 값(거절된 값)
bindingFailure : 타입 오류 같은 바인딩 실패인지?, 검증 실패인지? 를 구분 하는 값
(객체가 갖고 있는 맴버변수 자체에 바인딩이 실패했다면 true를 넘겨준다)
codes : 메시지 코드
arguments : 메시지에서 사용하는 인자
defaultMessage : 기본 오류 메시지
예를들어 가격부분에 범위를 넘어서는 가격을 추가했다고 해보자. 그렇다면 다음과 같은 FieldError가 추가될 것이다.
new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다.")
item.getPrice() 을 통해 rejectdValue를 등록하고 있다. 따라서 폼으로 전달되어 해당 값(사용자가 잘못 입력한 그 값) 을 다시 확인가능.
사용자의 입력 데이터가 컨트롤러의 @ModelAttribute 에 바인딩되는 시점에 오류가 발생하면 모델 객체에 사용자 입력 값을 유지하기 어렵다. 예를 들어서 가격에 숫자가 아닌 문자가 입력된다면 가격은 Integer 타입이므로 문자를 보관할 수 있는 방법이 없다.
그래서 오류가 발생한 경우 사용자 입력 값을 보관하는 별도의 방법이 필요하다.
그리고 이렇게 보관한 사용자 입력 값을 검증 오류 발생시 화면에 다시 출력하면 된다.
FieldError 는 오류 발생시 사용자 입력 값을 저장하는 기능을 제공한다.
● 스프링의 바인딩 오류 처리
타입 오류로 바인딩에 실패하면 스프링은 FieldError 를 생성하면서 사용자가 입력한 값을 넣어둔다.
new FieldError("item", "price", "문자입력", true, null, null, "Spring의 에러 메시지")
아마 위와 같이 인자가 넘어오는데, rejectedValue에는 사용자가 잘못 입력한 값 "문자입력" 을 저장하며,
이번에는 값의 바인딩이 정상적으로 되지 않는 오류이기 때문에 true를 적어준다.
그리고 해당 오류를 BindingResult 에 담은 후에서야 비로소 컨트롤러를 호출한다.
따라서 타입 오류 같은 바인싱 실패시에도 사용자의 오류 메시지를 정상 출력할 수 있다.
● 타임리프의 사용자 입력 값 유지
th:field="*{price}"
타임리프의 th:field 는 매우 똑똑하게 동작하는데, 정상 상황에는 모델 객체의 값을 사용하지만, 오류가 발생하면 FieldError 에서 보관한 값을 사용해서 값을 출력한다.
'BackEnd > Spring MVC' 카테고리의 다른 글
[Spring] 검증1 - Validation - 4 (0) | 2022.03.06 |
---|---|
[Spring] 검증1 - Validation - 3 (0) | 2022.03.06 |
[Spring] 검증1 - Validation - 1 (0) | 2022.03.05 |
[Spring] 메시지, 국제화 (0) | 2022.03.05 |
[Spring] 스프링 MVC - 기본 기능 - 3 (0) | 2022.02.28 |
댓글