Spring/Spring MVC

스프링 MVC 오류 코드와 메시지 처리

taey 2024. 10. 1. 17:44

errors 메시지 파일 생성

errors.properties라는 별도의 파일로 관리해보자.

 

스프링 부트 메시지 설정 추가

application.properties

spring.messages.basename=messages,errors

 

errors.properties 추가

required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

 

참고 : errors_en.properties 파일을 생성하면 오류 메시지도 국제화 처리를 할 수 있다.

 

예시 코드

@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
     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() > 10000) {
		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}";
}
//range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
new FieldError("item", "price", item.getPrice(), false, new String[] {"range.item.price"}, new Object[]{1000, 1000000}
  • codes : required.item.itemName를 사용해서 메시지 코드를 지정한다. 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
  • arguments : Object[]{1000, 1000000}를 사용해서 코드의 {0}, {1}로 치환할 값을 전달한다.

 


FieldError, ObjectError는 다루기가 번거롭다. 오류 코드를 자동화하려면?

rejectValue(), reject()

BindingResult가 제공하는 rejectValue(), reject()를 사용하면 FieldError, ObjectError를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.

 

예시 코드

@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
     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, 1000000}, null);
     }
    
	if (item.getQuantity() == null || item.getQuantity() > 10000) {
		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}";
}

 

rejectValue()

void rejectvalue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
  • field : 오류 필드명
  • errorCode : 오류코드 (이 오류 코드는 메시지에 등록된 코드가 아니다. 뒤에서 설명할 messageResolver를 위한 오류 코드이다.)
  • errorArgs : 오류 메시지에서 {0}을 치환하기 위한 값
  • defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null)

위의 코드를 실행하면

 

range.item.price=가격은 {0] ~ {1}까지 허용합니다.

 


범용적인 오류 메시지

required.item.itemName : 상품 이름은 필수입니다.

→ required: 필수 값입니다.

 

range.item.price: 상품의 가격 범위 오류입니다.

→ range : 범위 오류입니다.

 

오류 메시지에 객체명과 필드명을 조합한 세밀한 메시지 코드가 있으면 그 메시지를 높은 우선수위로 사용한다.

#Level1
required.item.itemName: 상품 이름은 필수 입니다.

#Level2
required: 필수 값 입니다.

 

스프링은 MessageCodesResolver라는 것으로 기능을 지원

 


MessageCodesResolver

package hello.itemservice.validation;

import org.junit.jupiter.api.Test;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;
import static org.assertj.core.api.Assertions.assertThat;

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의 기본 메시지 생성 규칙

객체 오류

객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code

예) 오류 코드: required, object name: item
1.: required.item
2.: required

 

필드 오류

필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
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를 통해서 생성된 순서대로 오류 코드를 보관한다.