@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
// 검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
// 검증 로직
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" th:classappend="${errors?.containsKey('itemName')} ? 'field-error' : _" class="form-control">
-->
<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>
@PostMapping("/add")
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.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다.));
}
}
// 검증 실패
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
<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">
<div class="field-error" th:errors="*{itemName}">상품명 오류</div>
</div>
// ObjectError도 유사한 생성자 제공
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)
spring.messages.basename = messages, errors
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
bindingResult.addError(new FieldError("item", "price",
item.getPrice(), false, new String[]{"range.item.price"},
new Object[]{1000, 1000000}, null));
// "가격은 1000 ~ 1000000 까지 허용합니다."
void rejectValue (@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
void reject(String errorCode, @Nullable Object[] errorArgs,
@Nullable String defaultMessage);
// 특정 field 예외 (itemName, price, quantity)
bindingResult.rejectValue("itemName", "required");
bindingResult.rejectValue("price","range", new Object[]{1000,1000000}, null);
bindingResult.rejectValue("quantity","max", new Object[]{9999}, null);
// 전체 예외
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
#==FieldError==
#Level1
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#Level2 - 생략
#Level3
required.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} 까지의 숫자를 허용합니다.
#Level4
required = 필수 값 입니다.
min= {0} 이상이어야 합니다.
range= {0} ~ {1} 범위를 허용합니다.
max= {0} 까지 허용합니다.
ValidationUtils.rejectIfEmptyOrWhitespace
(bindingResult, "itemName", "required");
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
private final ItemValidator itemValidator;
...
itemValidator.validate(item, bindingResult);
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValiators(itemValidator);
}
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
[출처] 스프링 MVC 2 - 김영한, 인프런