ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC - 검증(Validation) 직접 구현하기
    Spring/Spring MVC 2022. 2. 5. 21:25
    728x90
    반응형

     

     

     

     

    검증(Validation)

     

    검증 방법은 2가지가 있다. 클라이언트 검증 서버 검증이 있는데 둘의 장단점이 존재한다.

    • 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
       - ex) Postman으로 클라이언트 검증없이 요청이 가능
    • 서버만에서만 검증하면, 즉각적인 고객 사용성이 부족해진다.
       - 사용자가 입력 폼을 작성할 때 실시간으로 검증 오류를 나타낼수 없다.
    • 둘을 적절히 섞어서 사용하는게 좋다. 다만 최종적으로 서버 검증은 필수
    • API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨줘야 한다.

     

     검증을 사용할 때 주의할 점이 있는데 웹 애플리케이션은 폼 입력시 숫자를 문자로 작성하거나해서 검증 오류가 발생하면 오류 화면(4xx 상태 코드)으로 바로 이동한다. 이렇게 되면 사용자는 처음부터 해당 폼으로 다시 직접 이동해서 입력을 해야 한다. 아마도 이런 서비스라면 사용자는 금방 떠나버릴 것이다. 웹 서비스는 폼 입력시 검증 오류가 발생하면, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 친절하게 알려주어야 한다

     

     따라서 잘못된 입력으로 인한 오류 페이지로 가지않고 서버에서 검증을 통하여 어떤 값이 잘못 입력되었는지 다시 입력 폼 페이지에서 보여줘야하는데 이러한기능을 스프링에서 제공한다.

     


     

     스프링에서 제공하는 검증 기능이 있지만 검증이 어떻게 이루어지는지 알아보기위해 간단하게 검증을 구현해보자.

     

     

    검증 기능 구현(Spring의 검증 기능 사용 x)

     

    Controller에서 검증 기능 구현

     

     다음과 같은 검증 기능을 구현해보자.

    • 타입 검증
         - 가격, 수량에 문자가 들어가면 검증 오류 처리
    • 필드 검증
         - 상품명: 필수, 공백X
         - 가격: 1000원 이상, 1백만원 이하
         - 수량: 최대 9999
    • 특정 필드의 범위를 넘어서는 검증
         - 가격 * 수량의 합은 10,000원 이상

     

     

     

    1. 전송 받은 데이터가 개발자가 정의한 검증에 위반했을 경우 출력할 메시지를 저장하는 Map 선언
      Map<String, String> errors = new HashMap<>();​
    2. 전송 받은 데이터를 개발자가 정의한 검증 로직 구현
      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);
          }
      }​
      개발자가 정의한 검증을 통과하지 못할 경우 Map 컬렉션인 errors에 출력할 메시지 저장
          (key, value)  →   (filed, 출력 메시지)
      ▶ 특정 필드가 아닌 복합 룰에 대한 필드명은 globalError로 지정
    3. 검증에 위반한 필드가 있으면 다시 입력 폼으로 request 영역에 errors를 저장하고 응답 메시지 전송
      if (!errors.isEmpty()) {
          log.info("errors={}", errors);
          model.addAttribute("errors", errors);
          return "validation/v1/addForm";
      }​
    4. 검증에 위반한 필드가 없으면 정상적인 로직 처리
      Item savedItem = itemRepository.save(item);
      redirectAttributes.addAttribute("itemId", savedItem.getId());
      redirectAttributes.addAttribute("status", true);
      return "redirect:/validation/v1/items/{itemId}";​
    5. 전체 코드
      @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}";
      }​
    6. 사용자가 정의한 검증을 통과하지 못했을 경우 logging 결과로 확인

     

    HTML에 검증 위반에 따른 메시지 출력

     

     

     서버단에서 개발자가 정의한 검증 구현을 하였다. 이제 검증에 위반하였을 때 화면에 해당 검증 위반 메시지가 화면에 나타나도록 HTML을 수정해보자.

     

    1. 위에서 구현한 검증 중 globalError의 검증을 위반했을 시 화면에 에러메시지 렌더링
      <div th:if="${errors?.containsKey('globalError')}">
          <p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
      </div>​
       서버를 가동시켜 웹브라우저에서 HTML의 소스 코드를 보면 다음과 같다.
      <div>
          <p class="field-error">가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재값 = 1</p>
      </div>
      ▶ ${errors?.containsKey('globalError')
             - 처음 입력 폼을 렌더링했을 경우 errors는 null 값을 갖게 된다. 때문에
               errors.containsKey()를 호출하게 되면 NullPointerException이 발생한다.
               errors?. 는 errors가 null일 경우 예외가 발생하는 대신 null값을 반환하는 문법


    2. itemName의 필드가 검증을 위반했을 시 화면에 에러메시지 렌더링 
      <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>
      서버를 가동시켜 웹브라우저에서 HTML의 소스 코드를 보면 다음과 같다.
      <div>
          <label for="itemName">상품명</label>
          <input type="text" id="itemName" class="form-control field-error" placeholder="이름을 입력하세요" name="itemName" value="">
          <div class="field-error">상품 이름은 필수 입니다.</div>
      </div>
      ▶ price, quantity 필드도 똑같은 방법으로 에러 메시지를 화면에 출력할 수 있다.

     

    문제점

     

    • 뷰 템플릿에서 검증 메시지를 위한 코드 중복이 많다.
    • 타입 오류 처리가 불가하다.
         - price, quantity와 같은 필드는 Integer 타입이므로 만약 문자로 데이터가 넘어오면
          컨트롤러에 진입하기도 전에 예외가 발생하여 오류 페이지가 나오게된다.
    • 클라이언트에서 타입을 잘못 입력한 경우 데이터 유지 불가
         - 타입이 다른 데이터 값이 들어오면 해당 필드에 바인딩 할 수 없기 때문에
          입력 폼에서 입력한 문자가 사라지게 된다.
         - 클라이언트 입장에서는 어떤 입력으로 인한 오류가 발생하였는지 알 수 없다.

     

    728x90
    반응형

    댓글

Designed by Tistory.