검증 (Validation)

최준호·2021년 8월 29일
0

Spring

목록 보기
21/47

검증이란

사용자가 어플리케이션을 사용하며 어떠한 입력하는 동작에서 해당 값이 유효한 값인지 검사하는 것이다.

검증 직접 처리

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

        //검증 로직
        if(!StringUtils.hasText(item.getItemName())){
            errors.put("itemName","상품 이름은 필수입니다.");
        }
        if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice()>1000000){
            errors.put("price", "가격은 1,000원 ~ 1,000,000 까지 허용합니다.");
        }
        if(item.getQuantity() == null || item.getQuantity() >= 9999){
            errors.put("quantity", "수량은 최대 9,999 까지 허용합니다.");
        }

        //특정 필드가 아닌 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice() * item.getQuantity();
            if(resultPrice < 10000){
                errors.put("golbalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = "+resultPrice);
            }
        }

        //검증 실패시 다시 form 으로
        if(!errors.isEmpty()){  //부정의 부정은 코드의 가독성을 떨어트린다.
            model.addAttribute("errors", errors);
            return "validation/v1/addForm";
        }

        //검증에 성공

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v1/items/{itemId}";

검증을 직접 개발한다면 Map에 검증에 실패한 내용을 담아서 model로 넘겨야한다. 그리고 검증에 실패했을때 연결되는 페이지 또한 등록하는 페이지로 연결해야한다.

여기서 중요한 점은 form에서 넘기는 객체의 정보를 검증하는 로직에서도 가져가야한다는 것이다. model에 계속 객체를 가지고 있어야 만약 검증에 실패했을 때 보여지는 addForm에서 사용자가 입력했던 값들을 그대로 가지고 다시 출력해줄 수 있어야하기 때문이다.

실제로 자신의 템플릿 엔진에 맞게 설정하여 errors의 값이 존재할때 다음과 같이 출력해주면 된다.

위 같이 처리할때의 문제점

  1. 템플릿 상 중복 처리되는 부분이 많아 템플릿 코드가 복잡해진다.
  2. 타입 처리가 어렵다. 만약 숫자인데 문자가 들어온다면 컨트롤러에서 객체에 정보를 담을 때 터져버리기 때문에 사용자에게 400 오류 페이지가 나타난다.
  3. 타입 오류가 난다고 해도 사용자가 입력한 내용을 그대로 전달해주어야하는데 오류페이지가 나타나기 때문에 전달이 불가능하다.

Spring이 제공하는 BindingResult

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

        //검증 로직
        if(!StringUtils.hasText(item.getItemName())){
            bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수 입니다."));
        }
        if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice()>1000000){
            bindingResult.addError(new FieldError("item", "price", "가격은 1,000원 ~ 1,000,000 까지 허용합니다."));
        }
        if(item.getQuantity() == null || item.getQuantity() >= 9999){
            bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9,999 까지 허용합니다."));
        }

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

        //검증 실패시 다시 form 으로
        if(bindingResult.hasErrors()){
            return "validation/v2/addForm";
        }

        //검증에 성공

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v2/items/{itemId}";
    }

먼저 코드를 설명하자면 기존 위에 있던 코드는 @ModelAttribute에 Item을 가져와 Map에 오류를 key로 담고 메세지를 value로 담아서 반환했다면 위 코드는 스프링에서 제공하는 BindingResult를 사용하는 코드이다. 여기서 중요한 점은 BindingResult는 무조건 @ModelAtrribute의 뒤에 있어야 정상적으로 작동한다는 것이다. 그리고 front의 문법 또한 변환되므로 문법도 수정해주어야한다. 나의 예시는 타임리프 뿐이라 타임리프의 코드만 적어놓겠다. 그리고 BindingResult를 처음 보거나 front 문법이 이해가 안된다고 해서 걱정할 필요가 없다. 우리가 실무에서 사용하는 검증 방법은 아니니깐! (물론 사용하는 곳도 있겠지만)

	<div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메세지</p>
        </div>

        <div>
            <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
            <div class="field-error" th:errors="*{itemName}">상품명 오류</div>
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}" th:errorclass="field-error" class="form-control" placeholder="가격을 입력하세요">
            <div class="field-error" th:errors="*{price}">가격 오류</div>
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" th:errorclass="field-error" class="form-control" placeholder="수량을 입력하세요">
            <div class="field-error" th:errors="*{quantity}">수량 오류</div>
        </div>

위와 같이 작성 시 정상적으로 작동하는 것을 확인할 수 있다.

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글