Spring 검증 1

바그다드·2023년 5월 9일
0

검증

목록 보기
1/5

  • 이런 데이터 입력 폼이 있다고 하자. 상품명은 아마 string형식으로 받을 것이고, 가격이나 수량은 숫자형 데이터로 받을 것이다. 물론 html도 데이터 종류에 맞춰서 type을 설정해뒀을 것이다. 하지만 만약 데이터 형태가 서버와 약속한 형태가 아니라면? 에러가 발생할 것이다. 때문에 프론트에서 데이터 검증을 했다고 하더라도 서버에서도 검증을 해줄 필요가 있다. 코드로 확인해보자.

검증 순서

검증을 하는 순서는 아래와 같다.

  1. 클라이언트로부터 파라미터 값을 받는다.
  2. 서버에서 파라미터 타입을 검증을 한다.
  3. 검증을 통과하지 못할 경우 입력 페이지로 다시 이동한다.
    • 이때 받았던 파라미터 값을 다시 클라이언트로 넘겨줘야 한다.

Controller

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

        // 검증 오류 결과를 보관
        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("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
            }
        }

        // 검증에 실패하면 다시 입력 폼으로
        if (!errors.isEmpty()) {
            log.info("errors = {}", errors);
            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}";
    }
  • errors라는 맵을 생성하여 에러 메세지를 담고 이 값을 model에 담아준다.
  • 그런데 아까 말했듯이 기존에 받았던 파라미터 값을 다시 클라이언트로 넘겨줘야 한다고 했는데, 위의 코드에서는 파라미터로 넘어온 item을 다시 model에 담아주는 코드는 보이지 않는다. 그 이유는 @ModelAttribute 때문인데 @ModelAttribute는 객체를 생성하고, 각 속성에 파라미터 값을 setter를 이용하여 집어 넣은 후, model에 자동으로 집어 넣어준다. 따라서 따로 model에 담아주지 않아도 기존에 생성한 item이라는 값이 model에 담기게 된다!!
  • 조건문을 거치며 검증을 하고, 검증에 실패할 경우에는 에러 메세지를 추가하여 파라미터 값과 함께 입력 폼으로 이동시킨다.
    여기서는 따로 db에 값을 저장하는 상황이 아니므로 redirect를 하지 않고 단순히 이동만 시킨다. 그럼 이제 에러 메세지를 띄워주자.

form 생성

  • 아래 코드는 상품 등록 화면의 폼 부분만 자가져온 것이다.
    <form action="item.html" th:action th:object="${item}" method="post">

        <div th:if="${errors?.containsKey('globalError')}">
            <p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
        </div>

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

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/validation/v1/items}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>
  1. th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control'"
    삼항 연산자를 이용하여 errors에 각 속성 값이 존재한다면 에러를 강조하기 위한 class를 추가한다.
  2. th:if를 사용하여 마찬가지로 errors에 각 속성 값이 존재한다면 에러 메세지를 띄운다.

    th:if="${errors?.containsKey('quantity')}"

  • 위의 코드를 보면 ?가 들어있는데, 만약에 errors가 null이라고 한다면 저 코드는 결국 null.containsKey('quantity')이 되어 예외가 발생하게 된다.
    하지만 ?를 사용하면 errors가 null일 경우 그냥 null을 반환하기 때문에 예외가 발생하는 것을 피할 수 있다.
    - 프로젝트할 때 session값이 있는지 확인하는 코드에서 th:if문을 사용해도 에러가 발생했는데, 그것도 ?를 사용하면 쉽게 해결할 수 있는 문제였다ㅜ

문제점

  1. 중복되는 코드가 많다.
  2. string이야 숫자가 들어오든 문자가 들어오든 값이 들어오기만 하면 상관 없지만, price나 quantity같은 integer형식의 데이터의 경우에는 문자 값을 받는 것 자체가 안되기 때문에 컨트롤러를 호출하기 전에 에러가 발생한다.
  3. integer값에 문자가 들어온다고 하더라도 입력폼으로 돌려보낼 때 기존에 입력했던 값을 그대로 띄워줘야 한다.
  • 다음 포스팅에서 이 문제에 대해 지원하는 스프링의 기능을 알아보자.

출처 : 김영한님의 스프링MVC2

profile
꾸준히 하자!

0개의 댓글