Search
Duplicate
📒

[Spring Study] 06-1. Validation 처리 - BindingResult

상태
완료
수업
Spring Study
주제
Validation
4 more properties
참고

Validation 처리

NOTE
스프링 부트에서는 데이터 Validation을 위해 여러 접근 방식을 제공합니다. Validation 처리는 주로 데이터의 유효성을 확인하고, 타입 불일치 등의 입력 오류를 사용자에게 알려주는데 사용됩니다.
검증 처리에는 @ModelAttribute(form data)@RequestBody(JSON) 두 가지 타입이 있습니다. 이 두 요청 모두 @Valid, @Validated 어노테이션을 통해 처리 가능하지만 처리 방식에 차이가 있습니다.
@ModelAttribute: BindingResult를 사용하여 메서드 내에서 바로 오류를 처리할 수 있습니다.
@RequestBody: 검증 실패 시 MethodArgumentNotValidException 예외를 발생시켜 별도의 예외 처리기에서 관리합니다.

BindingResult

NOTE
BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체입니다.
@PostMapping("/add") public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult) { 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() >= 10000) { 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()) { return "validation/v2/addForm"; } // ... }
Java
복사
form 데이터 validation
BindingResult는 인터페이스이며 Errors를 상속받고 있습니다. Errors를 사용해도 되지만, BindingResult가 더 사용하기 쉽게 되어있습니다.
BindingResult 객체는 FiledErrorObjectError를 통해 발생한 오류들을 관리합니다. 만약 JSP, Thymeleaf를 사용하는 경우 오류 정보에 접근이 가능합니다.
FiledError: 특정 필드에 대한 검증 실패 정보
ObjectError: 객체 레벨의 오류(필드 에러가 아닌 객체검증)

오류 메시지 파일

NOTE
MessageSource는 스프링 프레임워크의 국제화 기능을 지원하는 인터페이스이며, 이를 통해 에러 메시지를 외부에서 관리할 수 있게됩니다.
spring: messages: basename: messages,errors
YAML
복사
errors.properteis, errors_[언어].properteis의 파일들을 사용할 수 있게됩니다.
required.item.itemName=상품 이름은 필수입니다. range.item.price=가격은 {0} ~ {1} 까지 허용합니다. max.item.quantity=수량은 최대 {0} 까지 허용합니다. totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
Java
복사
errors.properties
에러 메시지 등록
public class MessageService { @Autowired private MessageSource messageSource; public String getRequiredItemNameMessage(Locale locale) { return messageSource.getMessage("required.item.itemName", null, locale); } public String getRangeItemPriceMessage(int min, int max, Locale locale) { Object[] args = {min, max}; return messageSource.getMessage("range.item.price", args, locale); } public String getMaxItemQuantityMessage(int max, Locale locale) { Object[] args = {max}; return messageSource.getMessage("max.item.quantity", args, locale); } public String getTotalPriceMinMessage(int min, int current, Locale locale) { Object[] args = {min, current}; return messageSource.getMessage("totalPriceMin", args, locale); } }
Java
복사
메시지 사용코드

rejectValue, reject

NOTE
rejectValuereject는 BindingResult에서 제공하는 메서드로, 데이터 검증 과정에서 발생한 오류를 특정 필드나 전역적으로 등록하는데 사용됩니다.
// FieldError => rejectValue() // rejectValue( 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() >= 9999) { bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null); } // ObjectError => reject() if (item.getPrice() != null && item.getQuantity() != null) { int resultPrice = item.getPrice() * item.getQuantity(); if (resultPrice < 10000) { bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null); } }
Java
복사
void rejectValue(String field, // 오류 발생 필드이름 String errorCode, // 오류 코드, 화면에서 오류 메시지 찾기위한 키 값 Object[] errorArgs, // 메시지 인자값 String defaultMessage); // 메시지가 없는 경우 기본 메시지 void reject(String errorCode, // 전역 오류 코드, 화면에서 오류 메시지 찾기 위한 키 값 Object[] errorArgs, // 메시지 인자값, 메시지 포맷팅에 사용 String defaultMessage); // 메시지가 없는 경우 기본으로 사용될 메시지
Java
복사
rejectValue, reject 메서드 형태

MessageCodesResolver, DefaultMessageCodeResolver

NOTE
MessageCodesResolver와 구현체인 DefaultMessageCodeResolver는 스프링에서 유효성 검사를 할 때 어떤 오류 메시지를 찾을지 결정하는 역할을 진행합니다.
typeMismatch.item.name=상품 이름에는 문자열이어야 합니다. typeMismatch.item.price=상품 가격에는 숫자를 입력해야 합니다.
Java
복사
errors.properties
private final MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); @PostMapping("/items") public String addItem(@RequestBody Item item, BindingResult bindingResult) { // MessageCodesResolver를 사용하여 메시지 코드 생성 String[] errorCodes = messageCodesResolver.resolveMessageCodes("typeMismatch", "item.name"); // 생성된 메시지 코드를 이용하여 오류 메시지를 BindingResult에 추가 bindingResult.rejectValue("name", errorCodes[0]); // 오류가 있으면 폼 페이지로 다시 이동 if (bindingResult.hasErrors()) { return "itemForm"; } // 유효성 검사 통과 시, 비즈니스 로직 수행 // ... // 성공 시 다음 페이지로 리다이렉트 또는 응답 return "redirect:/success"; }
Java
복사

Validator 분리(Validator 구현, WebDataBinder)

NOTE
BindingResult의 복잡한 검증 로직을 별도로 분리하여 컨트롤러에서 간결하게 유지하기 위해서는 Validator 인터페이스를 구현하고, WebDataBinder에 등록하면 됩니다.
public interface Validator { boolean supports(Class<?> clazz); void validate(Object target, Errors errors); }
Java
복사
@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); } } } }
Java
복사
supports() : 해당 검증기를 지원하는지 여부
validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult
private final ItemValidator itemValidator; @PostMapping("/add") public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult) { // 검증 로직 단순화 itemValidator.validate(item, bindingResult); if (bindingResult.hasErrors()) { return "validation/v2/addForm"; } // 성공 로직 return "redirect:/validation/v2/items"; }
Java
복사
WebDataBinder를 사용하여 Validator를 등록하면 해당 컨트롤러에서 자동으로 검증이 수행됩니다.
private final ItemValidator itemValidator; @InitBinder public void init(WebDataBinder dataBinder){ dataBinder.addValidators(itemValidator); } // @Validated 어노테이션으로 자동으로 Validation 진행 @PostMapping("/add") public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return "validation/v2/addForm"; } // 성공 로직 return "redirect:/validation/v2/items"; }
Java
복사
위 코드에서 @Validated 대신 @Valid를 사용해도 정상적으로 동작합니다.
@Valid: 자바 표준 어노테이션
@Validated: 스프링 특화 어노테이션