spring

Validation V1

noAb 2024. 5. 12. 23:15

웹 사이트에서 입력폼을 통해 데이터를 서버로 보내야 할 때 특정조건에 맞춰서 입력해야하는 부분이 존재할 것이다.

한 예제로 주문을 넣는 폼이라고 한다면 주문 수량의 경우 숫자만 들어가야한다던지, 최소한 몇 이상의 수량을 작성을 해야한다는 등이다.

위와 같은 입력 폼이 있다고 했을때 가장 쉬운 방법으로는 Javascript를 사용하여 입력에 제한을 할 수도있고 

저장버튼을 눌렀을 때 입력값들을 체크하여 서버로 전송을 막을수도 있다.

하지만 Javascript만으로 클라이언트 검증만 하게 된다면 웹에 대해 조금만 공부한 사람이라면 얼마든지 취약점을 뚫을수 있고 보안에 매우 취약할 것이다. 이를 위해 우리는 서버에서 한번 더 검증(Validation)을 거쳐야한다.

 


 @PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, RedirectAttributes redirectAttributes , Model model) {
        //성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v2/items/{itemId}";
    }

POST 메서드로 add라는 매핑으로 들어오게 되면 입력 폼에서 받아온 값들을 item에 저장하여 Item이라는 곳에 저장한 뒤, 라다이렉트를 햇을때 itemId를 다시 가지고가서 화면에 보여주는 코드이다.

전혀 검증이 이루어지지 않은 코드이다.


Validation V1 본문에서는 가장 쉬운 방법으로 입력폼에서 받아온 Item의 값들을 하나씩 if문을 사용하여 조건에 맞지 않으면 error에 저장하는 코드를 작성해보자.

@PostMapping("/add")
    public String addItemV(@ModelAttribute Item item, BindingResult bindingResult, 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){ //상품가격이 1000에서 1000000사이가 아닐경우
            errors.put("price", "가격은 1,000 ~ 1,000,000 까지 허용합니다.");
        }
        if(item.getQuantity()==null|| item.getQuantity() >= 9999) { //수량이 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";
     	}

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

기존의 코드에서 if문을 사용하여 우리가 원하는 값이 아닌경우 Errors에 put한 모습이다.

모든 검증이 끝이났다면 errors를 확인하여 값이 잇다면 errors를 model에 저장한 뒤 리턴을 하고, 그렇지 않다면 그대로 저장하여 저장된 상세페이지로 이동을 하게 된다.

그렇다면 뷰 페이지에서는 어떻게 표현을 할지 확인해보자.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }

        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="py-5 text-center">
        <h2 th:text="#{page.addItem}">상품 등록</h2>
    </div>

    <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'"
                   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'"
                   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'"
                   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>
</div> <!-- /container -->
</body>
</html>

위 아래에 지워도 되는 기본적인 HTML문법도 잇지만 신경쓰지말고 해당 주요영역을 보게 되면 기본적으로 타입리프를 사용하여 form태그에 item오브젝트를 넣고 그 하위에 각 itemName, price, quantity를 필드로 넣은 모습이다.

아무런 문제 없이 값을 기입하고 저장을 햇다면 해당 필드에 값이 잘 보이게 될텐데 error가 발생 할 경우 th:if문에 errors를 비교하여 해당 이름에 맞는 오류문구가 태그에 보이게 될 것이다. 

th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}"
위와 같은 문법을 사용했는데 해석하자면 errors(컨트롤러에서 에러가 잇다면 put한 객체)의 key값으로 
itemName이 존재한다면 이라는 의미이다. 만약 존재한다면 해당 태그에 th:text를 통해 itemName이라는 key값의 value를
태그에 기입하게 되며 보이게 될 것이다.

입력 데이터 폼에 아무값도 넣지 않고 저장버튼을 누르게 되면 Controller에서 만든 if문에서 각 필드에모든 에러가 key와 value로 저장되게 되고 그 값을 가지고 다시 폼으로 이동하여 th:if문에서 true가 되며 해당 문구가 보이는 모습니다.

 

Validation V1에서는 가장 단순하면서 쉬운 Map을 사용하여 에러를 담고 뿌려준 모습이지만, Spring과 부트에서는 보다 쉽고 범용성 잇는 기능이 존재한다. V2, V3를 더 작성하여 한 단계씩 발전하는 Validation의 모습을 기록하려고한다.

 


정리하자면,
우리는 웹사이트에서 입력을 받을 때 조건에 맞는 검증을 해야하며 프론트 검증과 서버 검증을 모두 거쳐야한다.
프론트 검증은 javascript로 구현이 가능하지만 보안에 취약한 반면
서버 검증은 즉각 대응이 어렵다는 단점이 있다.
이를 위해 우리는 모두 사용할 줄 알아야 한다.

 

p.s 해당 글은 인프런 김영한님의 spring 활용편 내용을 기록한다.