BindingResult
- BindingResult는 검증 오류가 발생할 경우 오류 내용을 보관하는 스프링 프레임워크에서 제공하는 객체다.
- 스프링이 제공하는 검증 오류를 보관하는 객체이다.
- 검증 오류가 발생하면 여기에 보관하면 된다.
- BindingResult 파라미터의 위치는 @ModelAttribute 파라미터 다음에 와야한다.
- BindingResult가 없으면 @ModelAttribute 에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다.
- 원래 같았으면 400에러로 오류 페이지로 넘어가야 되지만, BindingResult를 넣어주면 오류에 대한 결과가 BindingResult 객체에 담기고 컨트롤러가 호출된다.
- BindingResult에 검증 오류를 적용하는 3가지 방법
- @ModelAttribute의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 FieldError를 생성해서 BindingResult에 넣어준다.
- 개발자가 오류를 직접 넣는다.
- Validator를 사용한다.
- 타입 오류로 바인딩에 실패하면 스프링이 FieldError를 생성하면서 사용자가 입력한 값을 넣어둔다. -> 그리고 해당 오류를 BindingResult에 담아서 컨트롤러를 호출한다. -> 이렇게 되면 타입 오류같은 바인팅 실패시에도 사용자의 오류 메시지를 정상적으로 출력할 수 있다.
- 위에서 언급한 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 : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
- codes : 메시지 코드
- arguments : 메시지에서 사용하는 인자
- defaultMessage : 기본 오류 메시지
- 이 중에서 codes, arguments 는 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.
- src/main/resources/errors.properties 를 추가하고 아래와 같이 사용할 수 있다.위와 같이 errors.properties를 작성했다면 아래와 같이 사용할 수 있다.
range.item.price=가격은 {0}~{1}까지 허용합니다.
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 100000}, null));
- bindingResult.getObject() : target 객체의 이름
- bindingResult.getTarget() : target 객체
- bindingResult.rejectValue("price", "range", new Object[]{1000, 100000}, null) : rejectValue를 사용하면 FieldError를 더 간단하게 구현할 수 있다.
- 구현 예시는 아래와 같다.
@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
}
- 위에 "range"는 MessageCodesResolver 인터페이스 덕분에 error,properties에 정의된 메시지를 사용할 수 있는 것이다.
- validation 클래스를 따로 분리해서 구현하면 아래와 같이 구현할 수 있다.
@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);
}
}
}
- @Component 를 달아줘서 빈에 등록시킨 후 검증기에서 사용할 수 있다.
- 검증기를 구현해서 컨트롤러 호출시 무조건 실행되게 설정할 수 있다. -> 컨트롤러를 매번 호출할때마다 검증하는 코드를 작성할 필요가없다. 구현은 아래와 같이 할 수 있다.
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(itemValidator);
}
- 위와 같이 검증기를 사용하기 위해서는 컨트롤러에 @Validated를 적어줘야 한다.
- @Validated 는 검증기를 실행하라는 애노테이션이다. -> WebDataBinder에 등록된 검증기를 찾아서 실행한다.
- @Valid 를 사용해도 된다.
- @Validated 는 스프링 전용 애노테이션이고, @Valid 는 자바 전용 애노테이션이다.
REFERENCES