2 분 소요


검증

컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것이다.

클라이언트 검증, 서버 검증

  • 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
  • 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
  • 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수.
  • API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함

검증 직접 처리

validation
고객이 등록 폼에서 필드에 값을 입력하지 않거나, 수량이 적은 등 문제 발생시 검증에 실패하게 된다. 실패한 경우 고객에게 다시 상품 등록 폼을 보여주고 문제된 부분을 알려줘야 한다.


직접 처리 개발

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()) {
        model.addAttribute("errors", errors);
        return "validation/v1/addForm";
    }

// 성공 로직은 생략..

}

필드 오류
검증시 오류가 발생하면 errors에 담아두고, 오류를 구분하기 위해 발생한 필드명을 key로 사용했다.

복합 룰 검증
특정 필드를 넘어서 오류를 처리해야 할 수도 있지만, 이때는 필드명을 넣을 수 없으므로 globalError라는 key를 사용했다.

addForm.html

<!-- 오류시 사용할 style 추가 -->
.field-error {
    border-color: #dc3545;
    color: #dc3545;
}

<!-- 글로벌 오류 메시지 -->
<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>
<!-- 나머지 필드 생략... -->

글로벌 오류 메시지

오류 메시지는 errors에 내용이 있을 때만 출력하면 된다.

[참고] Safe Navigation Operator
등록폼에 진입한 시점(GET)에는 errors가 생성되지 않았다.
따라서, errors.containsKey()를 호출하게 되면 NullPointerException이 발생한다.

errors?errorsnull일때 NullPointerException이 발생하는 대신, null을 반환하는 문법이다.
th:if에서 null은 실패로 처리되므로 오류 메시지가 출력되지 않는다.

필드 오류 처리

위 코드는 조건문을 사용해서 오류가 있을 시에 class명 뒤에 field-error붙여서 구분해주었지만 classappend를 사용하면 간편하게 수정할 수 있다.

<input type="text" th:classappend="${errors?.containsKey('itemName')} ? 'field-error' : _"
    class="form-control">

classappend를 사용해서 해당 필드에 오류가 있으면 field-error라는 클래스 정보를 더해서 색깔을 빨간색으로 강조. 만약 오류가 없다면 _(No-Operation)을 사용해서 아무것도 하지 않는다.


정리

  • 만약 검증 오류가 발생하면 입력 폼을 다시 보여준다.
  • 검증 오류들을 고객에게 안내해서 다시 입력할 수 있게 한다.
  • 검증 오류가 발생해도 고객이 입력한 데이터가 유지된다.
    • @ModelAttribute Item item


문제점

  • 뷰 템플릿에서 중복 처리가 많다.
  • 타입 오류 처리가 안된다.(IntegerString값 입력)
    • 이런 오류는 컨트롤러에 진입하기도 전에 예외가 발생하여 컨트롤러가 호출되지도 않고, 400예외가 발생하게 된다.
  • 만약 컨트롤러가 호출된다고 해도 Integer이므로 문자를 보관할 수가 없다.
  • 해결 : 고객이 입력한 값이 어딘가에 별도로 관리가 되어야 한다.


<출처 : 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한)>

댓글남기기