-
[스프링MVC - 2편] 검증 (2)스프링&스프링부트 2025. 2. 9. 19:27
오류 코드와 메시지 처리 1
errors 메시지 파일 생성
errors.properties라는 별도의 파일로 관리한다
FieldError, ObjectError의 생성자는 codes, arguments를 제공하며 이것은 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다
스프링 부트 메시지 설정 추가
// application.properties spring.messages.basename=messages,errors // src/main/resources/errors.properties required.item.itemName=상품 이름은 필수입니다. range.item.price=가격은 {0} ~ {1} 까지 허용합니다. max.item.quantity=수량은 최대 {0} 까지 허용합니다. totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
2-4. ValidationItemControllerV2 - addItemV3() 추가
@PostMapping("/add") public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { // 검증 로직 if (!StringUtils.hasText(item.getItemName())) { bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null)); } if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){ bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null)); } if (item.getQuantity() == null || item.getQuantity() >= 9999) { bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]{9999}, null)); } // 특정 필드가 아닌 복합 룰 검증 if (item.getPrice() != null && item.getQuantity() != null) { int resultPrice = item.getPrice() * item.getQuantity(); if (resultPrice < 10000) { bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10000, resultPrice}, null)); } } // 검증에 실패하면 다시 입력 폼으로 이동 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}"; }
오류 코드와 메시지 처리 2
컨트롤러에서 BindingResult는 검증해야 할 객체인 target 바로 다음에 온다
따라서 BindingResult는 이미 본인이 검증해야 할 객체인 target을 알고 있다
log.info("objectName={}", bindingResult.getObjectName()); log.info("target={}", bindingResult.getTarget()); // 출력 결과 // objectName=item //@ModelAttribute name // target=Item(id=null, itemName=상품, price=100, quantity=1234)
2-5. ValidationItemControllerV2 - addItemV4() 추가
// 검증 로직 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); } // 특정 필드가 아닌 복합 룰 검증 if (item.getPrice() != null && item.getQuantity() != null) { int resultPrice = item.getPrice() * item.getQuantity(); if (resultPrice < 10000) { bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null); } }
rejectValue()
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
- field : 오류 필드명
- errorCode : 오류 코드 (messageResolver)
- errorArgs : 오류 메시지에서 {0}을 치환하기 위한 값
- defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
reject()
void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
오류 코드와 메시지 처리 3
오류 코드와 메시지 처리 4
MessageCodesResolverTest
public class MessageCodesResolverTest { MessageCodesResolver codesResolver = new DefaultMessageCodesResolver(); @Test void messageCodesResolverObject() { String[] messageCodes = codesResolver.resolveMessageCodes("required", "item"); assertThat(messageCodes).containsExactly("required.item", "required"); } @Test void messageCodesResolverField() { String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class); assertThat(messageCodes).containsExactly( "required.item.itemName", "required.itemName", "required.java.lang.String", "required" ); } }
MessageCodesResolver
- 검증 오류 코드로 메시지 코드들을 생성한다
- MessageCodesResolver 인터페이스이고 DefaultMessageCodesResolver는 기본 구현체이다
- 주로 ObjectError, FieldError과 함께 사용
DefaultMessageCodesResolver의 기본 메시지 생성 규칙
1. 객체 오류
1. : code + "." + object name
2. : code
예) 오류 코드: required, object name: item
1. : required.item
2. : required
2. 필드 오류
1. : code + "." + object name + "." + field
2. : code + "." + field
3. : code + "." + field type
4. : code
예) 오류 코드: typeMismatch, object name "user", field "age", field type: int
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"4. "typeMismatch"
동작 방식
- rejectValue(), reject()는 내부에서 MessageCodesResolver를 사용하고 여기에서 메시지 코드들을 생성한다
- FieldError, ObjectError의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다
- MessageCodesResolver를 통해서 생성된 순서대로 오류 코드를 보관한다
- BindingResult의 로그 : codes [range.item.price, range.price, range.java.lang.Integer, range]
FieldError - rejectValue("itemName", "required")
다음 4가지 오류 코드를 자동으로 생성
- required.item.itemName
- required.itemName
- required.java.lang.String
- required
ObjectError - reject("totalPriceMin")
다음 2가지 오류 코드를 자동으로 생성
- totalPriceMin.item
- totalPriceMin
오류 코드와 메시지 처리 5
ValidationUtils
// ValidationUtils 사용 전 if (!StringUtils.hasText(item.getItemName())) { bindingResult.rejectValue("itemName", "required","기본: 상품 이름은 필수입니다."); } // ValidationUtils 사용 후 // 다음과 같이 한줄로 가능, 제공하는 기능은 Empty, 공백 같은 단순한 기능만 제공 ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
정리
1. rejectValue() 호출
2. MessageCodesResolver를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3. new FieldError() 생성하면서 메시지 코드들을 보관
4. th:erros에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출
오류 코드와 메시지 처리 6
Validator 분리1
복잡한 검증 로직을 별도로 분리한다
ItemValidator
@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; // 검증 로직 if (!StringUtils.hasText(item.getItemName())) { errors.rejectValue("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() >= 9999) { 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); } } } }
- supports() {} : 해당 검증기를 지원하는 여부 확인
- validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult
ValidationItemControllerV2 - addItemV5()
@PostMapping("/add") public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { itemValidator.validate(item, bindingResult); // 검증에 실패하면 다시 입력 폼으로 이동 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}"; }
Validator 분리2
WebDataBinder 사용
WebDataBinder는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다
ValidationItemControllerV2
@InitBinder public void init(WebDataBinder dataBinder) { log.info("init binder {}", dataBinder); dataBinder.addValidators(itemValidator); }
ValidationItemControllerV2 - addItemV6()
@PostMapping("/add") public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) { 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}"; }
동작 방식
@Validated는 검증기를 실행하라는 애노테이션이다
이 애노테이션이 붙으면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행한다 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다
이때 supports()가 사용되며 여기서는 supports(Item.class)호출되고, 결과가 true이므로 ItemValidator의 validate()가 호출된다
728x90'스프링&스프링부트' 카테고리의 다른 글
[스프링MVC - 2편] 검증 (4) (0) 2025.03.02 [스프링MVC - 2편] 검증 (3) (1) 2025.03.02 [스프링MVC - 2편] 검증 (1) (0) 2025.02.08 [스프링MVC - 2편] 메시지, 국제화 (0) 2025.02.04 [스프링MVC - 2편] 타임리프 스프링 통합 (2) (1) 2025.02.02