API 방식 사용 시, API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨야 함
BindingResult
스프링이 제공하는 검증 오류를 보관하는 객체
@PostMapping("/add")publicStringaddItem(@ModelAttributeItem item,BindingResult bindingResult,RedirectAttributes redirectAttributes,Model model) {if (!StringUtils.hasText(item.getItemName())) {bindingResult.addError(newFieldError("item","itemName",item.getItemName(),false,null,null,"상품 이름은 필수 입니다.")); }if (item.getPrice() ==null||item.getPrice() <1000||item.getPrice() >1000000) {bindingResult.addError(newFieldError("item","price",item.getPrice(),false,null,null,"가격은 1,000 ~ 1,000,000 까지 허용합니다.")); }if (item.getQuantity() ==null||item.getQuantity() >=9999) {bindingResult.addError(newFieldError("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(newObjectError("item",null,null,"가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = "+ resultPrice)); } }// 검증 실패 시 입력 폼으로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 class * 필드에 오류가 있으면 FieldError 객체를 생성해서 bindingResult 에 담아두자. */publicFieldError(String objectName,// @ModelAttribute 이름String field,// 오류가 발생한 필드 이름String defaultMessage // 오류 기본 메시지 ) {}publicFieldError(String objectName,// 오류가 발생한 객체 이름String field,// 오류 필드 @NullableObject rejectedValue,// 사용자가 입력한 값(거절된 값)boolean bindingFailure,// 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값 @NullableString[] codes,// 메시지 코드 @NullableObject[] arguments,// 메시지에서 사용하는 인자 @NullableString defaultMessage // 기본 오류 메시지)/** * ObjectError * 특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서 bindingResult 에 담아두자. */publicObjectError(String objectName,// @ModelAttribute 의 이름String defaultMessage // 오류 기본 메시지) {}
BindingResult
@ModelAttribute 객체 다음 위치
BindingResult 가 있으면 @ModelAttribute 에 데이터 바인딩 시 오류가 발생해도 컨트롤러 호출
BindingResult 가 없으면 400 오류 발생 후, 오류 페이지로 이동
BindingResult 가 있으면 오류 정보(FieldError)를 BindingResult 에 담아서 컨트롤러 정상 호출
Model 에 자동으로 포함
어떤 객체를 대상으로 검증하는지 타겟을 알고 있음
.
BindingResult 에 검증 오류를 적용하는 세 가지 방법
@ModelAttribute 객체의 타입 오류 등, 바인딩 실패 시 스프링이 FieldError 생성 후 BindingResult 에 삽입
개발자가 직접 입력
Validator 사용
Error Message
오류 메시지 파일을 인식할 수 있도록 설정 추가
spring.messages.basename=messages,errors
errors.properties
required.item.itemName=상품 이름은 필수입니다.range.item.price=가격은 {0} ~ {1} 까지 허용합니다.max.item.quantity=수량은 최대 {0} 까지 허용합니다.totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}required.default=기본 오류 메시지required=필수 값 입니다.
bindingResult.addError(newFieldError("item","itemName",item.getItemName(),false,newString[]{"required.item.itemName"},null,null));bindingResult.addError(newFieldError("item","price",item.getPrice(),false,newString[]{"range.item.price"},// codes : 메시지 코드newObject[]{1000,1000000},// arguments : 메시지에서 사용하는 인자null));
rejectValue() , reject()
BindingResult 는 검증해야 target 객체를 알고 있음
BindingResult 의 rejectValue(), reject() 를 사용하면 FieldError, ObjectError 를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있음
bindingResult.rejectValue("itemName","required");bindingResult.rejectValue("price","range",newObject[]{1000,10000000},"상품 가격 오류");bindingResult.reject("totalPriceMin",newObject[]{10000, resultPrice},null);...voidrejectValue( @NullableString field,// 오류 필드명String errorCode,// 오류 코드(메시지에 등록된 코드가 아닌 messageResolver 를 위한 오류 코드) @NullableObject[] errorArgs,// 메시지에서 사용하는 인자 @NullableString defaultMessage // 기본 메시지(오류 메시지를 찾을 수 없을 때 사용));voidreject(String errorCode, @NullableObject[] errorArgs, @NullableString defaultMessage);
Apply Thymeleaf
필드 오류 처리
rejectedValue(): 오류 발생 시 사용자 입력 값을 저장
bindingResult.rejectValue("price","range",newObject[]{1000,10000000},"상품 가격 오류");
MessageCodesResolver 는 인터페이스, DefaultMessageCodesResolver 는 기본 구현체
ObjectError, FieldError 와 주로 함께 사용
DefaultMessageCodesResolver 기본 메시지 생성 규칙
필드 오류
필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
1. code + "." + ObjectName + "." + field
2. code + "." + field
3. code + "." + field type
4. code
---
(example)
error code: required
ObjectName: item
field: itemName
field type: String
1. "required.item.itemName"
2. "required.itemName"
3. "required.java.lang.String"
4. "required"
객체 오류
객체 오류의 경우 다음 순서로 2가지 생성
1. code + "." + objectName
2. code
---
(example)
error code: totalPriceMin
ObjectName: item
1. "totalPriceMin.item"
2. "totalPriceMin"
동작 방식
rejectValue() , reject() 는 내부에서 MessageCodesResolver 사용 ➜ 여기서 메시지 코드 생성
FieldError, ObjectError 생성자를 보면 알 수 있듯이, 여러 오류 코드를 가질 수 있음
MessageCodesResolver 를 통해 생성된 순서대로 오류 코드 보관
1. rejectValue() 호출
2. MessageCodesResolver 를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3. new FieldError() 를 생성하면서 메시지 코드들을 보관
4. th:erros 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고 출력
오류 코드 관리 전략
구체적인 것 ➜ 덜 구체적인 것으로가 핵심
MessageCodesResolver 는 구체적인 것(required.item.itemName)을 먼저 생성하고, 덜 구체적인 것(required)을 나중애 생성
중요하지 않은 메시지는 범용성 있는 간단한 메시지(requried)로, 중요한 메시지는 필요할 때 구체적으로 사용하는 방식이 효과적
errors.properties
#==ObjectError==#Level1totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}#Level2- 생략totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}#==FieldError==#Level1required.item.itemName=상품 이름은 필수입니다.range.item.price=가격은 {0} ~ {1} 까지 허용합니다.max.item.quantity=수량은 최대 {0} 까지 허용합니다.#Level2- 생략#Level3required.java.lang.String= 필수 문자입니다.required.java.lang.Integer= 필수 숫자입니다.min.java.lang.String= {0} 이상의 문자를 입력해주세요.min.java.lang.Integer= {0} 이상의 숫자를 입력해주세요.range.java.lang.String= {0} ~ {1} 까지의 문자를 입력해주세요.range.java.lang.Integer= {0} ~ {1} 까지의 숫자를 입력해주세요.max.java.lang.String= {0} 까지의 숫자를 허용합니다.max.java.lang.Integer= {0} 까지의 숫자를 허용합니다.#Level4required = 필수 값 입니다.min= {0} 이상이어야 합니다.range= {0} ~ {1} 범위를 허용합니다.max= {0} 까지 허용합니다.
ValidationUtils
ValidationUtils 사용 전
if (!StringUtils.hasText(item.getItemName())) {bindingResult.rejectValue("itemName","required","기본: 상품 이름은 필수입니다.");}
WebDataBinder 는 스프링의 파라미터 바인딩 역할을 해주고, 검증 기능도 내부에 포함
/** * @InitBinder * 특정 컨트롤러에만 적용 */@InitBinderpublicvoidinit(WebDataBinder dataBinder) {dataBinder.addValidators(itemValidator);}.../** * Application.class * 글로벌 설정(모든 컨트롤러에 다 적용) * 직접 사용하는 경우는 드묾 */@OverridepublicValidatorgetValidator() {returnnewItemValidator();}