ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [김영한 스프링] 23. 검증1 Validation - 오류 코드와 메시지 처리1, 2, 3
    Spring/스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 2023. 9. 1. 03:41

    오류 코드와 메시지 처리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)

    파라미터 목록

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

     

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

     

    errors 메시지 파일 생성

    messages.properties를 사용해도 되지만, 오류 메시지를 구분하기 쉽게 errors.properties라는 별도의 파일로 관리해 보자.

     

    먼저 스프링 부트가 해당 메시지 파일을 인식할 수 있게 다음 설정을 추가한다. 이렇게 하면 messages.properties, errors.properties 두 파일을 모두 인식한다. (생략하면 messages.properties를 기본으로 인식한다.)

     

     

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

    spring.messages.basename=messages,errors

     

     

    errors.properties 추가

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

    src/main/resources/errors.properties 생성

     

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

     

     

    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("error = {} ", 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}";
        }

    코드 변경

    • addItemV2()의 @PostMapping 부분 주석 처리

     

    new FieldError("item", "price", item.getPrice(), false,
                             new String[] {"range.item.price"}, new Object[]{1000, 1000000}
    • codes : required.item.itemName를 사용해서 메시지 코드를 지정한다. 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
      • new String[] {"range.item.price1", "range.item.price2"}
      • "range.item.price1"이 없으면 "range.item.price2"를 찾도록 하기 위해 배열을 사용
    • arguments : Object[]{1000, 1000000}를 사용해서 코드의 {0} , {1}로 치환할 값을 전달한다.

     

     

    실행

     

     

    오류 코드와 메시지 처리2

     

    목표

    • FieldError, ObjectError는 다루기 너무 번거롭다.
    • 오류 코드도 좀 더 자동화할 수 있지 않을까? 예) item.itemName처럼?

     

    컨트롤러에서 BindingResult는 검증해야 할 객체인 target 바로 다음에 온다. 따라서 BindingResult는 이미 본인이 검증해야 할 객체인 target을 알고 있다.

    @ModelAttribute Item, item 뒤에 BindingResult bingingResult가 위치하기 때문에 objectName이 "item"인것을 이미 알고 있음

     

     

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

     

     

    결과

     

     

    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());
    
            // 검증 로직
            if (!StringUtils.hasText(item.getItemName())) {
    //            bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
                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);
                }
            }
    
            // 검증에 실패하면 다시 입력 폼으로
            if (bindingResult.hasErrors()) {
                log.info("error = {} ", 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}";
        }

    코드 변경

    • addItemV3()의 @PostMapping 부분 주석 처리

     

    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)

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

     

    축약된 오류 코드

    FieldError()를 직접 다룰 때는 오류 코드를 range.item.price 와 같이 모두 입력했다. 그런데 rejectValue()를 사용하고 나서부터는 오류 코드를 range로 간단하게 입력했다. 그래도 오류 메시지를 잘 찾아서 출력한다. 무언가 규칙이 있는 것처럼 보인다. 이 부분을 이해하려면 MessageCodesResolver를 이해해야 한다. 왜 이런 식으로 오류 코드를 구성하는지 바로 다음에 자세히 알아보자.

     

     

    실행

     

     

    오류 코드와 메시지 처리3

     

    오류 코드를 만들 때 다음과 같이 자세히 만들 수도 있고,

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

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

     

    또는 다음과 같이 단순하게 만들 수도 있다.

    required : 필수 값입니다.

    range : 범위 오류입니다.

     

    단순하게 만들면 범용성이 좋아서 여러곳에서 사용할 수 있지만, 메시지를 세밀하게 작성하기 어렵다. 반대로 너무 자세하게 만들면 범용성이 떨어진다. 가장 좋은 방법은 범용성으로 사용하다가, 세밀하게 작성해야 하는 경우에는 세밀한 내용이 적용되도록 메시지에 단계를 두는 방법이다.

     

    예를 들어서 required라고 오류 코드를 사용한다고 가정해 보자.

    다음과 같이 required라는 메시지만 있으면 이 메시지를 선택해서 사용하는 것이다

    required : 필수 값입니다.

     

    그런데 오류 메시지에 required.item.itemName와 같이 객체명과 필드명을 조합한 세밀한 메시지코드가 있으면 이 메시지를 높은 우선순위로 사용하는 것이다.

    new String[]{"required.item.itemName", "required"}

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

    #Level2
    required : 필수 값입니다.

     

    물론 이렇게 객체명과 필드명을 조합한 메시지가 있는지 우선 확인하고, 없으면 좀 더 범용적인 메시지를 선택하도록 추가 개발을 해야겠지만, 범용성 있게 잘 개발해 두면, 메시지의 추가 만으로 매우 편리하게 오류 메시지를 관리할 수 있을 것이다.

     

    스프링은 MessageCodesResolver라는 것으로 이러한 기능을 지원한다.

     

     

    결과

     

     

    출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

     

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

    웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

    www.inflearn.com

Designed by Tistory.