BindingResult
이전 편에서 HashMap을 통하여 Error를 들고 날랐다면 스프링에서는 그를 대체할 BindingResult라는 클래스가 존재합니다.
@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
}
Controller에서 BindingResult를 사용하여 기존 Error를 담도록 별도 배열을 생성할 필요가 없습니다.
대신 BindingResult bindingResult 파라미터의 위치는 @ModelAttribute 다음에 위치해야 합니다.
bindingResult.addError(new FieldError("objectName", "field", "defaultMessage"));
필드에 오류가 있으면 bindingResult의 addError()에 FieldError객체를 추가하여 저장합니다.
- objectName : @ModelAttribute 이름
- field : 오류가 발생한 필드 이름
- defaultMessage : 오류 기본 메시지
만약 필드에 해당하지 않고 복합적인 검증 에러나 글로벌 에러를 처리하기 위해서는 ObjectError를 사용합니다.
FieldError와 다르게 Object 그 자체에서 에러가 발생한 것이기 때문에 별도의 field를 넣어줄 필요가 없습니다.
public ObjectError(String objectName, String defaultMessage) {}
이렇게 담긴 BindingResult는 Model에 담겨 View에서 사용할 수 있습니다.
#fields라는 키워드로 담긴 에러에 접근할 수 있으며 GlobalErrors를 통해 글로벌 오류가 존재하는지 확인할 수 있습니다.
<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>
1. 에러에 따른 class 변경 로직이 깔끔해진다.
th:errorclass="field-error"는 th:field의 *{itemName}의 itemName이 bindingResult값에 오류가 존재하면 "field-error"라는 클래스를 추가해준다.
2. 오류 존재 유무에 따른 삼항 연산자가 사라진다.
이전에는 itemName의 필드에 오류가 있으면 th:text="${errors ['itemName']}"으로 꺼내서 메시지를 출력하는 if문이 필요했지만 현재는 th:errors="*{itemName}"을 통해 깔끔하게 정리할 수 있다.
타임리프는 스프링 검증 오류 통합 기능이 있다. 위 링크에는 검증과 오류 메시지에서는 공식 매뉴얼이 있으니 참고.
BindingResult 오류 처리
스프링이 제공하는 검증 오류를 보관하는 객체로 오류를 보관한다. 특이한 점은 BindingResult의 파라미터가 존재하면 @ModelAttribute에 데이터 바인딩 시 오류가 발생해도 컨트롤러의 로직이 실행된다.
만약 BindingResult 객체 파라미터에 없고 타입 오류가 발생해 데이터 바인딩 오류가 발생하면 기존에는 400 오류를 반환하면서 컨트롤러를 호출하지 않지만, 파라미터에 BindingResult객체가 존재한다면 오류 정보(Field Error)를 담아 컨트롤러를 정상 호출한다.
검증 오류를 적용하는 3가지 방법
- @ModelAttribute의 객체에 오류가 발생해 바인딩 실패하는 경우 Spring이 FieldError 객체를 생성해 BindingResult에 넣어준다.
- 비즈니스의 검증에 따라 개발자의 구현하여 넣어준다.
- Validator를 사용한다.
BindingResult와 Errors
org.springframework.validation.Errors
org.springframework.validation.BindingResult
BindingResult는 인터페이스이고, Errors 인터페이스를 상속받고 있다.
실제 넘어오는 구현체는 BeanPropertyBindingResult라는 것인데, 둘 다 구현하고 있으므로 BindingResult 대신에 Errors를 사용해도 된다.
Errors 인터페이스는 단순한 오류 저장과 조회 기능을 제공한다.
BindingResult는 여기에 더해서 추가적인 기능들을 제공한다. addError() 도 BindingResult 가 제공하므로 여기서는 BindingResult를 사용하자. 주로 관례상 BindingResult를 많이 사용한다.
BindingResult에 넣는 FieldError의 파라미터를 확인해보자.
BindingResult객체를 #Fields에서 오류를 검출할 때 rejectValue에 값을 넣으면 오류가 발생한 값 자체를 보존해서 사용자에게 보여줄 수 있다.
boolean bindingFailure을 객체의 필드 값에 값 바인딩 자체가 실패했는지 여부이다.
code, arguments는 defaultMessage를 대체할 수 있는 방법이다.
이처럼 BindingResult는 두 가지의 생성자를 가지고 있으며 인자수가 많은 overload 된 생성자로 객체 생성 시 다양한 기능을 사용할 수 있다.
codes, arguments 인자를 사용해서 Error Message를 일관적으로 관리해보자.
totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==FieldError==
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
1. codes는 String []을 인자로 넣을 수 있다. 그 값은 errors.properties의 지정된 key값이다.
2. arguments는 Object []를 인자로 넣을 수 있다. 이 값은 errors.properties의 {}로 표현된 인자에 들어간다.
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false , new String[]{"required.item.itemName"}, null,"상품 이름은 필수입니다." ));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false , new String[]{"range.item.price"}, new Object[]{1000, 1000000},"가격은 1,000 ~ 1,000,000 까지 허용합니다." ));
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false , new String[]{"max.item.quantity"}, new Object[]{9999},"수량은 최대 9,999까지 허용합니다." ));
}
생각해보니 FielError와 ObjectError를 다루는 게 너무 번거롭다. BindingResult는 검증해야 할 객체 바로 다음에 위치한다. 이는 BindingResult객체는 이미 자신이 검증해야 할 target을 알고 있음을 뜻한다.
log.info("item : {} ", bindingResult.getObjectName());
log.info("target : {} ", bindingResult.getTarget());
rejectValue(), reject()
- 두 메서드를 사용하여 기존 로직과 변경된 로직을 비교해보자.
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false , new String[]{"required.item.itemName"}, null,"상품 이름은 필수입니다." ));
bindingResult.rejectValue("itemName", "required", "defaultMessage");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false , new String[]{"range.item.price"}, new Object[]{1000, 1000000},"가격은 1,000 ~ 1,000,000 까지 허용합니다." ));
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, "defaultMessage");
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false , new String[]{"max.item.quantity"}, new Object[]{9999},"수량은 최대 9,999까지 허용합니다." ));
bindingResult.rejectValue("quantity", "max", new Object[]{9999}, "defaultMessage");
}
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"},new Object[]{10000, resultPrice},"가격과 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
bindingResult.reject("totalPriceMin",new Object[]{10000, resultPrice},"defaultMessage");
}
}
* bindingResult의 errorCode파라미터는 errors.properties의 등록된 코드가 아니라 messageResolver를 위한 코드이다
FieldError , ObjectError 객체를 만들어 사용하는 것 대신 rejectValue, reject를 사용하면 FieldError, ObjectError객체를 만들어서 모든 값을 세팅해주는 메서드이다.
이 두메서드에 오류 코드(errorCode) 인자에 값을 넣는 것이 상당히 특이하다. errors.properties에 등록된 이름이 아니기 때문이다. 이는 MessageCodesResolver를 이해해야 한다.
'Spring|Spring-boot' 카테고리의 다른 글
Spring Converter (0) | 2022.01.10 |
---|---|
Spring MessageCodesResolver (0) | 2022.01.08 |
Spring Web Validation (1) (0) | 2022.01.08 |
Spring 메시지, 국제화 (0) | 2022.01.06 |
Spring MVC HTTP Header 처리와 Arguments, Return (0) | 2021.12.29 |
댓글