-
Spring MVC - 검증(Validation)에 errors MessageSource 사용Spring/Spring MVC 2022. 2. 6. 16:25728x90반응형
검증(Validation)에 errors MessageSource 사용
검증에 위반할 때 오류 코드에 대한 defaultMessage를 매번 적어서 사용하는 것보단 메시지 파일을 만들어 체계적으로 다루는 방법이 효율적이다. errors.properties 메시지 파일을 만들어 어떻게 이용하는지 점진적으로 하나씩 알아보자.
MessageSource 설정 추가
- MessageSource가 자동으로 errors.properties 파일을 읽어오기 위한 설정을 추가해야 한다.
- application.properties
▶ MessageSource는 기본으로 messages.properties만 읽어온다. 따라서 errors.properties도spring.messages.basename=messages,errors
읽어올 수 있도록 설정에 errors 도 추가해줘야 한다.
errors 메시지 파일 생성
- 에러 메시지를 관리하기 위한 파일
- 에러 메시지도 국제화 처리를 할 수 있다.
- errors.properties
▶ 예제 코드에 사용할 errorCode와 error message 작성required.item.itemName=상품 이름은 필수입니다. range.item.price=가격은 {0} ~ {1} 까지 허용합니다. max.item.quantity=수량은 최대 {0} 까지 허용합니다. totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
FieldError, ObjectError를 이용한 에러 메시지 파일 사용
FieldError, ObjectError는 errorCode로 해당 에러 메시지를 가져올 수 있도록 구현되어 있다. 아래 각각의 생성자를 살펴보자.
FieldError 의 생성자
public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure, @Nullable String[] codes, @NullableObject[] arguments, @Nullable String defaultMessage) {...}
- objectName : 오류가 발생항 객체 이름
- field : 오류 필드
- rejectedValue : 사용자가 입력한 값(거절된 값)
- bindingFailure : 타입 오류 같은 바이딩 실패인지, 검증 실패인지 구분 값
- codes : 메시지 코드
- arguments : 메시지에서 사용하는 인자
- defaultMessage : 기본 오류 메시지
ObjectError 의 생성자
public ObjectError( String objectName, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) {...}
- objectName : 오류가 발생항 객체 이름
- codes : 메시지 코드
- arguments : 메시지에서 사용하는 인자
- defaultMessage : 기본 오류 메시지
FieldError, ObjectError 클래스를 사용하여 원하는 errorCode로 errors.properties 파일에서 해당 error message를 가져올 수 있다.
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.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)); } }
▶ codes : required.item.itemName 를 사용하여 errorCode 지정
errorCode는 배열로 여러 값을 전달할 수 있는데 순서대로 매칭 하여 처음 매칭 되는 메시지를 사용
▶ arguments : Objec 배열을 사용하여 메시지에 전달 인자를 전달하여 치환됨
( {0}, {1} → {1000, 1000000} )
( {0}, {1} → {10000, resultPrice} )더보기전체 코드
@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}"; }
rejectValue(), reject()로 에러 메시지 파일 사용
위 방법보다 BindingResult가 제공하는 rejectValue(), reject() 를 사용하면 FieldError, ObjectError를 직접 생성하지 않고 깔끔하게 검증 오류를 다룰 수 있다.
FieldError, ObjectError를 직접 생성하지 않고 BindingResult가 제공하는 rejectValue(), reject() 를 사용할 수 있는 이유는 스프링이 처리해주기 때문이다. BindingResult는 검증해야 할 객체(target) 바로 다음에 온다. 따라서 BindingResult는 이미 본인이 검증해야 할 객체인 target을 컨트롤러가 호출될 때 이미 알고 있는 상태이다.
ex)
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
BindingResult가 검증 객체를 알고 있는지 logging을 이용하여 확인해보자.
log.info("objectName={}", bindingResult.getObjectName()); log.info("target={}", bindingResult.getTarget());
위 결과와 같이 BindingResult는 컨트롤러가 호출되는 시점에 이미 검증 객체(objectName)을 이미 알고 있다는 것을 확인할 수 있다.
rejectValue(), reject()
- rejectValue(), reject() 메서드는 내부적으로 FieldError, ObjectError를 생성해준다.
- 이미 objectName을 알고 있기 때문에 코드 작성이 용이하다.
- 메서드 정보
void rejectValue(@Nullable String field, String errorCode); void rejectValue(@Nullable String field, String errorCode, String defaultMessage); void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); void reject(String errorCode); void reject(String errorCode, String defaultMessage); void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
- field : 오류 필드명
- errorCode : 오류 코드
- errorArgs : 오류 메시지에 전달할 전달 인자
- defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
rejectValue(), reject() 메서드를 사용하여 원하는 errorCode로 errors.properties 파일에서 해당 error message를 가져올 수 있다.
//bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수 입니다.")); bindingResult.rejectValue("itemName", "required"); //bindingResult.addError(new ObjectError("item",new String[]{"totalPriceMin"} ,new Object[]{10000, resultPrice}, null)); bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
참고로 ValidationUtils은 내부적으로 if 조건에 대한 처리가 메서드로 구현되어있어 사용하기 편리하다.
if (!StringUtils.hasText(item.getItemName())) { bindingResult.rejectValue("itemName", "required"); } //위 코드와 동일 ValidationUtils.rejectIfEmptyOrwhitespace(bindingResult, "itemName", "required");
더보기전체 코드
@PostMapping("/add") public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) { log.info("objectName={}", bindingResult.getObjectName()); log.info("target={}", bindingResult.getTarget()); 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, 10000000}, 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); } } //검증에 실패하면 다시 입력 폼으로 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}"; }
MessageCodesResolver
- rejectValue, reject 내부에서 errorCode로 메시지 코드들을 생성해주는 인터페이스
- DefaultMessageCodesResolver를 구현체로 사용한다.
- 생성한 메시지 코드들을 FieldError, ObjectError의 매개변수에 String[] 타입으로 인자를 전달하여 생성
(FieldError, ObjectError의 매개변수 String[] codes 에 생성된 메시지 코드들을 전달) - rejectValue("필드명", "에러코드") 에서 생성되는 메시지 코드
우선순위 메시지 코드 생성 방법 예시( rejectValue("itemName", "required") ) 1 code + "." + object name + "." + field required.item.itemName 2 code + "." + field required.itemName 3 code + "." + field type required.java.lang.String 4 code required - reject("에러코드") 에서 생성되는 메시지 코드
우선순위 메시지 코드 생성 방법 예시( reject("totalPriceMin") ) 1 code + "." + object name totalPriceMin.item 2 code totlaPriceMin - 타임리프 화면을 렌더링 할 때 th:errors가 오류가 있다면
오류 메시지 코드로 우선순위 순서대로 해당 메시지를 찾는다.
errors.properties 예시
#==ObjectError== #우선순위 1 totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1} #우선순위 2 totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1} #==FieldError== #우선순위 1 required.item.itemName=상품 이름은 필수입니다. range.item.price=가격은 {0} ~ {1} 까지 허용합니다. max.item.quantity=수량은 최대 {0} 까지 허용합니다. #우선순위 2 - 생략 #우선순위 3 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} 까지의 숫자를 허용합니다. #우선순위 4 required = 필수 값 입니다. min= {0} 이상이어야 합니다. range= {0} ~ {1} 범위를 허용합니다. max= {0} 까지 허용합니다. #스프링에서 자동으로 생성하는 오류 코드에 대한오류 메시지 또한 사용자 정의로 만들 수 있다. typeMismatch.java.lang.Integer=숫자를 입력해주세요. typeMismatch=타입 오류입니다.
728x90반응형'Spring > Spring MVC' 카테고리의 다른 글
Spring MVC - Bean Validation(애노테이션을 이용한 검증) (0) 2022.02.07 Spring MVC - Validator 분리와 구현 (0) 2022.02.06 Spring MVC - 스프링에서 제공하는 검증(Validation) 방법 (FieldError, ObjectError) (0) 2022.02.05 Spring MVC - 검증(Validation) 직접 구현하기 (0) 2022.02.05 Spring MVC - 타임리프로 MessageSource에 등록된 메시지 사용하기 (0) 2022.02.05