오봉이와 함께하는 개발 블로그

스프링 MVC 2 - 오류 코드와 메시지 처리 part.1 본문

BE/Spring

스프링 MVC 2 - 오류 코드와 메시지 처리 part.1

오봉봉이 2022. 8. 23. 02:20
728x90

오류 코드와 메시지 처리 1

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

FieldError, ObjectError의 생성자는 codes, arguments를 제공한다. 이것은 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.

errors 메시지 파일 생성

messages.properties를 사용해도 되지만, 오류 메시지를 구분하기 쉽게 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}

이제 메시지를 사용하도록 코드를 변경해보자.

ValidationItemControllerV2 - addItemV3() 추가

    @PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        // 검증 오류 결과 보관
        Map<String, String> errors = new HashMap<>();

        // 검증 로직
        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}, "수량은 최대 9,999개 까지 허용합니다."));
        }
        // 특정 필드가 아닌 복합 룰 검증
        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}";
    }

codes : required.item.itemName를 사용해서 메시지 코드를 지정한다. 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
arguments : new Object[]{1000, 1000000}를 사용해서 코드의 {0}, [1]로 치환할 값을 전달한다.

오류 코드와 메시지 처리 2

FieldError, ObjectError는 다루기 너무 번거롭다. 오류 코드도 좀 더 자동화 할 수 있는 방법을 적용해보자.

컨트롤러에서 BindingResult는 자기가 검증해야 할 객체인 target바로 다음에 온다. 따라서 BindingResult는 이미 본인이 검증해야 할 객체인target을 알고 있다.
-> @ModelAttribute Item item, BindingResult bindingResult

// 출력
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());

// 결과
objectName=item //@ModelAttribute name
target=Item(id=null, itemName=상품, price=100, quantity=1234)

rejectValue(), reject()

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

ValidationItemControllerV2 - addItemV4() 추가

    @PostMapping("/add")
    public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        log.info("objectName={}", bindingResult.getObjectName());
        log.info("target={}", bindingResult.getTarget());

        // 검증 오류 결과 보관
        Map<String, String> errors = new HashMap<>();

        // 검증 로직
        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[]{1000}, 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}";
    }

errors.properties에 있는 코드를 직접 입력하지 않았는데 실행하면 오류 메시지가 정상 출력된다.

rejectValue()

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

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

BindingResult는 어떤 객체를 대상으로 검증하는지 target을 이미 알고 있기 때문에 target에 대한 정보는 없어도 된다. 필드 오류명은 동일하게 price를 사용했다.

축약된 오류 코드

FieldError()를 직접 다룰 때는 오류 코드를 range.item.price와 같이 모두 입력했다.
그런데 rejectValue()를 사용하고 부터는 range로 간단하게 입력해도 오류 메시지를 잘 찾아서 출력한다.
이 부분을 이해하려면 MessageCodesResolver를 이해해야 한다.

출처 : 인프런 김영한 지식공유자님 강의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술
728x90
Comments