컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것
클라이언트 검증(주로 자바스크립트), 서버 검증
컨트롤러
//검증 로직
if(!StringUtils.hasText(item.getItemName())){
errors.put("itemName","상품 이름은 필수입니다.");
}
if(item.getPrice()==null||item.getPrice()<1000||item.getPrice()>1000000){
errors.put("price","가격은 1,000~1,000,000 까지 허용합니다.");
}
if(item.getQuantity()==null||item.getQuantity()>=9999){
errors.put("quantity","수량은 최대 9,999 까지 허용합니다.");
}
//특정 필드가 아닌 복합 룰 검증
if(item.getPrice()!=null&& item.getQuantity()!=null){
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice<10000){
errors.put("globalError",
"가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = "+resultPrice);
}
}
//검증에 실패하면 다시 입력 폼으로
if(!errors.isEmpty()){
log.info("errors={}",errors);
model.addAttribute("errors",errors);
return "validation/v1/addForm";
}
뷰 템플릿
<!--글로벌 오류 처리-->
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error"
th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
<!--필드 오류 처리-->
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('itemName')}"
th:text="${errors['itemName']}">
상품명 오류
</div>
</div>
errors.containsKey()
를 호출하는 순간 NullPointerException
이 발생errors?.
은 errors 가 null 일때 NullPointerException
이 발생하는 대신, null 을 반환하는 문법스프링이 제공하는 검증 오류 처리 방법
bindingResult.addError(new FieldError(@ModelAttribute 이름,오류가 발생한 필드 이름, 오류 기본 메시지));
bindingResult.addError(new ObjectError(@ModelAttribute 의 이름, 오류 기본 메시지));
public String addItemV1
(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//검증 로직
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원 이상이어야 합니다. 현재 값 = "+resultPrice ));
}
}
//검증에 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()){
log.info("errors={}",bindingResult);
return "validation/v2/addForm";
}
...
}
타임리프는 스프링의 BindingResult 를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공
글로벌 오류 처리
<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>
BindingResult
가 있으면 @ModelAttribute
에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출됨 > 뭐가 문제가 있는지 여기에 담김BindingResult에 검증 오류를 적용하는 3가지 방법
1. @ModelAttribute 의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 FieldError 생성해서 BindingResult에 넣어줌 (스프링이 자동 처리)
2. 개발자가 직접 넣어줌 (검증 로직)
3. Validator 사용
FieldError 생성자
bindingResult.addError(
new FieldError("item","price",item.getPrice(),
false,null,null,"가격은 1,000~1,000,000 까지 허용합니다."));
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);
오류 발생 시 사용자 입력 값 유지할 수 있는 이유
스프링의 바인딩 오류 처리
errors.properties
spring.messages.basename=messages,errors
//range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
bindingResult.addError(
new FieldError("item","price",item.getPrice(),false,
new String[]{"range.item.price"},new Object[]{1000,1000000},null));
컨트롤러에서 BindingResult 는 검증해야 할 객체인 target 바로 다음에 옴 > BindingResult 는 이미 본인이 검증해야 할 객체를 알고 있음
BindingResult가 제공하는 rejectValue() , reject()를 사용하면 FieldError , ObjectError를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있음
bindingResult.rejectValue("price","range", new Object[]{1000,1000000},null);
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
required 오류 코드를 사용한다고 가정할 때
#Level1
required.item.itemName: 상품 이름은 필수 입니다.
#Level2
required: 필수 값 입니다.
//필드 에러일 때 (에러 코드, 객체 이름, 필드, 필드 타입)
String[] messageCodes =
codesResolver.resolveMessageCodes
("required", "item", "itemName", String.class);
//객체 에러일 때 (에러 코드, 객체 이름)
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
객체 오류
객체 오류의 경우 다음 순서로 2가지 생성
1. : code + "." + object name
2. : code
필드 오류
필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
1. : code + "." + object name + "." + field
2. : code + "." + field
3. : code + "." + field type
4. : code
자세한 순서에서 덜 자세한 순서로 생성
rejectValue("itemName", "required")
에서 MessageCodesResolver를 호출하여 4가지 오류 코드를 자동으로 생성하고 오류 코드들을 넣은 새로운 FieldError을 생성 reject("totalPriceMin")
에서 MessageCodesResolver를 호출하여 2가지 오류 코드를 자동으로 생성하고 오류 코드들을 넣은 새로운 ObjectError을 생성 정리
1. rejectValue() 호출
2. MessageCodesResolver 를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3. new FieldError() 를 생성하면서 메시지 코드들을 보관
4. th:erros 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출
검증 오류 코드 종류
1. 개발자가 직접 설정한 오류 코드 rejectValue() 를 직접 호출한 경우
2. 스프링이 직접 검증 오류에 추가한 경우 (주로 타입 정보가 맞지 않음)
typeMismatch.item.price
typeMismatch.price
typeMismatch.java.lang.Integer
typeMismatch
복잡한 검증 로직을 별도로 분리하기 위해서는 별도의 클래스로 역할을 분리
Validator를 구현하는 클래스를 생성
supports() {}
: 해당 검증기를 지원하는 여부 확인validate(Object target, Errors errors)
: 검증 대상 객체와 BindingResult, 검증 로직 여기에 넣음생성한 클래스를 스프링 빈으로 주입받아 컨트롤러에서 호출
컨트롤러에서 검증기를 자동으로 적용하는 법 > WebDataBinder를 통해서 사용
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(itemValidator);
//이 컨트롤러가 호출될 때 마다 항상 불려져서 검증기를 적용
}
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// @Validated를 넣어주면 해당 객체(item)에 대해서 자동으로 검증기가 실행 됨
// 검증 다 하고 그 결과를 BindingResult에 담음
...
}
- 이 애노테이션이 붙으면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행
- 여러 검증기를 등록한다면 supports()를 사용하여 그 중에 어떤 검증기가 실행되어야 할지 구분