2 분 소요


목표

  • 사용자 입력 오류 메시지가 화면에 남아야 한다.
  • FieldError, ObjectError에 대한 더 자세한 내용.


addItemV2

@PostMapping("/add")
public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
    
    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수입니다."));
    }

    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
    }

    // 수량 생략..

    //특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.addError(new ObjectError("item", null, null, "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
        }
    }
}

FieldError

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)
  • objectName: 오류가 발생한 객체 이름
  • field: 오류 필드
  • rejectedValue: 사용자가 입력한 값(거절된 값)
  • bindingFailure: 검증 실패 구분 값(타입 오류 바인딩 실패인지…)
  • codes: 메시지 코드
  • arguments: 메시지에서 사용하는 인자
  • defaultMessage: 기본 오류 메시지

타임리프의 사용자 입력 값 유지
th:field="*{price}"
타임리프의 th:field는 정상 상황에서는 모델 객체의 값을 사용하지만, 오류 발생시 FieldError에서 보관한 값을 사용해서 값을 출력한다.

스프링의 바인딩 오류 처리
타입 오류로 바인딩 실패시 스프링은 FieldError를 생성하여 사용자가 입력한 값을 넣어두고, 해당 오류를 BindingResult에 담아서 컨트롤러를 호출한다. 따라서 타입 오류 같은 바인딩 실패시에도 오류 메시지를 정상 출력할 수 있다.
bindingResult.addError(new FieldError("item", "price", "qqq", ...))


addItemV3

FieldError, ObjectError의 생성자는 errorCode, arguments를 제공하는데, 이는 오류 발생시 오류 코드로 메시지를 찾기 위함이다.

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

spring.messages.basename=messages, errors

생략하면 messages.properties를 기본으로 인식한다.

errors.properties

errors.properties

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

Controller

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를 사용해서 메시지 코드 지정. 메시지 코드는 배열로 여러 값을 전달할 수 있어, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
  • arguments: Object[]{1000, 1000000}를 사용해서 코드의 {0}, {1}로 치환할 값을 전달한다.

실행하면 MessageSource를 찾아서 메시지를 조회한다.


addItemV4

  • FieldError, ObjectError는 사용하기 번거로운 감이 있다.
  • 그래서, 오류 코드도 좀 더 자동화 시키기로 했다.

BindingResult는 검증해야 할 객체(target)바로 다음에 온다.
따라서, BindingResult는 이미 검증해야 할 객체인 target을 알고있다.

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

Controller

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.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);

BindingResulttarget을 이미 알고 있기 때문에 target(item)에 대한 정보는 없어도 된다.

reject()

void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String  defaultMessage);

축약된 오류 코드
rejectValue()를 사용하니 오류 코드를 range로 간단하게 입력했다. 이 부분은 MessageCodesResolver를 이해해야 한다.


<출처 : 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한)>

댓글남기기