코드 그라데이션

검증 직접 처리 본문

Spring/Validation

검증 직접 처리

완벽한 장면 2024. 2. 20. 23:51

검증 직접 처리 - 소개

상품 저장 성공

  • 사용자가 상품 등록 폼에서 정상 범위의 데이터를 입력하면, 서버에서는 검증 로직이 통과하고, 상품을 저장하고,     상품 상세 화면으로 redirect한다.

일반적인 상품 저장 성공 매커니즘


상품 저장 검증 실패

  • 고객이 상품 등록 폼에서 상품명을 입력하지 않거나, 가격, 수량 등이 너무 작거나 커서 검증 범위를 넘어서면,          서버 검증 로직이 실패해야 한다. 이렇게 검증에 실패한 경우 고객에게 다시 상품 등록 폼을 보여주고,                       어떤 값을 잘못 입력했는지 친절하게 알려주어야 한다.

등록 실패(오류) 시 매커니즘

 


개발 시작

ValidationItemControllerV1 - addItem() 수정

// 실제 저장하는 곳
@PostMapping("/add")
public String addItem(@ModelAttribute Item item,
                      RedirectAttributes redirectAttributes,
                      Model model) { // 모델 추가

    // 검증 오류 결과 보관
    Map<String, String> errors = new HashMap<>();
    // => 만약 검증 시 오류가 발생하면 어떤 검증에서 발생했는지 정보를 담아둔다.

    // 검증 로직
    if (!StringUtils.hasText(item.getItemName())) { // StringUtils 임포트 추가(글자가 없냐)
        errors.put("itemName", "상품 이름은 필수입니다."); // 키, 값 형태-> 나중에 화면에서 보여줄 것
    }
    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 100000) {
        errors.put("price", "가격은 1,000 ~ 1,000,000 까지 허용합니다.");
    }
    if (item.getQuantity() == null || item.getQuantity() >= 9999) {
        errors.put("quantity", "수량은 최대 9,999 까지 허용합니다.");
    }

    // 특정 필드가 아닌 복합 룰 검증(특정 필드의 범위를 넘어서는 검증 로직)
    //이때는 필드 이름을 넣을 수 없으므로 globalError 라는 key 를 사용한다.
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();

        if (resultPrice < 10000) {
            errors.put("globalError", "[가격 X 수량]의 합은 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}";
}

 

코드 하나씩 따보기

# 검증 오류 보관

Map<String, String> errors = new HashMap<>();
  • 만약 검증 시 오류가 발생하면 어떤 검증에서 발생했는지 정보를 담아둔다.

 

# 검증 로직

    // 검증 로직
    if (!StringUtils.hasText(item.getItemName())) { // StringUtils 임포트 추가(글자가 없냐)
        errors.put("itemName", "상품 이름은 필수입니다."); // 키, 값 형태-> 나중에 화면에서 보여줄 것
    }
  • 검증시 오류가 발생하면 errors 에 담아둔다. 
  • 이 때 어떤 필드에서 오류가 발생했는지 구분하기 위해 오류가 발생한 필드명을 key 로 사용한다. 
  • 이후 뷰에서 이 데이터를 사용해서 고객에게 친절한 오류 메시지를 출력할 수 있다.

 

# 특정 필드의 범위를 넘어서는 검증 로직

    // 특정 필드가 아닌 복합 룰 검증(특정 필드의 범위를 넘어서는 검증 로직)
    //이때는 필드 이름을 넣을 수 없으므로 globalError 라는 key 를 사용한다.
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();

        if (resultPrice < 10000) {
            errors.put("globalError", "[가격 X 수량]의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
        }
    }
  • 특정 필드를 넘어서는 오류를 처리해야 할 수도 있다. 
  • 이 때는 필드 이름을 넣을 수 없으므로 globalError 라는 key 를 사용한다.

 

# 검증에 실패하면 다시 입력 폼으로

    // 검증 실패 시 다시 입력 폼으로 돌려보내기
    if (!errors.isEmpty()) { // 부정의 부정(=>긍정)이니까 읽는 데에 유의
        log.info("errors = {}", errors);
        model.addAttribute("errors", errors);
        return "validation/v1/addForm";
    }
  • 만약 검증에서 오류 메시지가 하나라도 있으면 오류 메시지를 출력하기 위해 model 에 errors 를 담고,
    입력 폼이 있는 뷰 템플릿으로 보낸다.

HTML 폼 수정

<!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;
        }
        <!--css 추가 -->
        .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">

        <!--추가 -->
        <!-- 여기서는 if가 쓰였으므로 조건이 있을 때만 HTML 태그 출력 가능 -->
        <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>
</div> <!-- /container -->
</body>
</html>

 

하나씩 뜯어보기

# 글로벌 오류 메시지

  • 오류 메시지는 errors 에 내용이 있을 때만 출력하면 된다. 
  • 타임리프의 th:if 를 사용하면 조건에 만족할 때만 해당 HTML 태그를 출력할 수 있다.

 

 

필드 에러 실행하면!

콘솔 메시지는

에러 메시지 확인할 수 있다.

 

# 필드 오류 처리

방법은 두개 이다.

1)

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

2) 지금처럼

 

# 필드 오류 처리 - 메시지

  • 글로벌 오류 메시지에서 로직과 동일하, 필드 오류를 대상으로 한다.

 

상품 수정 검증은 더 효율적인 검증 처리 방법을 학습한 다음에 진행한다.

 

정리

 

남은 문제점

 

지금부터 스프링이 제공하는 검증 방법을 하나씩 알아보겠다.

728x90

'Spring > Validation' 카테고리의 다른 글

BindingResult 두 번째  (0) 2024.02.22
BindingResult 첫 번째  (0) 2024.02.22
프로젝트 준비 V2  (0) 2024.02.21
초창기 샘플코드  (0) 2024.02.20
검증 요구사항 및 프로젝트 생성(V1)  (0) 2024.02.19
Comments