BindingResult
가 있으면 @ModelAttribute
에 데이터 바인딩 시 오류가 발생해도 컨트롤러가주의
BindingResult bindingResult
파라미터의 위치는@ModelAttribute Item item
다음에 와야 한다.
FieldError
를 생성하면서 사용자가 입력한 값을 넣어둔다. BindingResult
에 담아서 컨트롤러를 호출한다. new FieldError("item", "price", item.getPrice(), false, new String[] {"range.item.price"}, new Object[]{1000, 1000000})
th:field="*{price}"
타임리프의 th:field
는 매우 똑똑하게 동작하는데, 정상 상황에는 모델 객체의 값을 사용하지만, 오류가
발생하면 FieldError
에서 보관한 값을 사용해서 값을 출력한다.
messages.properties
를 사용해도 되지만, 오류 메시지를 구분하기 쉽게 errors.properties
라는
별도의 파일로 관리해보자.
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
BindingResult
는 검증해야 할 객체인 target
바로 다음에 온다. 따라서 BindingResult
는 이미 본인이 검증해야 할 객체인 target
을 알고 있다.rejectValue()
, reject()
를 사용하면 FieldError
, ObjectError
를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}
FieldError()
를 직접 다룰 때는 오류 코드를 range.item.price
와 같이 모두 입력했다. 그런데
rejectValue()
를 사용하고 부터는 오류 코드를 range 로 간단하게 입력했다. 그래도 오류 메시지를 잘
찾아서 출력한다.
rejectValue()
, reject()
는 내부에서 MessageCodesResolver
를 사용한다. 여기에서 메시지
코드들을 생성한다.
FieldError
, ObjectError
의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다.
MessageCodesResolver
를 통해서 생성된 순서대로 오류 코드를 보관한다.
이 부분을 BindingResult
의 로그를 통해서 확인해보자.
codes [range.item.price, range.price, range.java.lang.Integer, range]
rejectValue("itemName", "required")
다음 4가지 오류 코드를 자동으로 생성
required.item.itemName
required.itemName
required.java.lang.String
required
reject("totalPriceMin")
다음 2가지 오류 코드를 자동으로 생성
타임리프 화면을 렌더링 할 때 th:errors
가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지
코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다.
MessageCodesResolver
는 required.item.itemName
처럼 구체적인 것을 먼저 만들어주고,
required
처럼 덜 구체적인 것을 가장 나중에 만든다.
이렇게 하면 앞서 말한 것 처럼 메시지와 관련된 공통 전략을 편리하게 도입할 수 있다.
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은 필수입니다.");
}
다음과 같이 한줄로 가능, 제공하는 기능은 Empty , 공백 같은 단순한 기능만 제공
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
}
supports() {}
: 해당 검증기를 지원하는 여부 확인validate(Object target, Errors errors)
: 검증 대상 객체와 BindingResult
WebDataBinder
는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.
@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
이렇게 WebDataBinder 에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.
@InitBinder
-> 해당 컨트롤러에만 영향을 준다.
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
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}";
}
@Validated
가 붙었다.@Validated
는 검증기를 실행하라는 애노테이션이다.
이 애노테이션이 붙으면 앞서 WebDataBinder
에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를
등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports()
가 사용된다.
여기서는 supports(Item.class)
호출되고, 결과가 true
이므로 ItemValidator
의 validate()
가
호출된다.