minOS

Bean Validation - 오브젝트 오류,수정에 적용,한계 본문

TIL/김영한의 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술

Bean Validation - 오브젝트 오류,수정에 적용,한계

minOE 2024. 9. 18. 09:30
728x90

오브젝트 오류

Bean Validation에서 특정 필드( `FieldError` )가 아닌 해당 오브젝트 관련 오류( `ObjectError` )는 어떻게 처리할 수 있을까?
`@ScriptAssert()` 를 사용하면 된다.

 @Data
 @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >=
 10000")
 public class Item {
//...
}​



실행해보면 정상 수행되는 것을 확인할 수 있다.
메시지 코드도 다음과 같이 생성된다.
메시지 코드
`ScriptAssert.item`
`ScriptAssert`


그런데 실제 사용해보면 제약이 많고 복잡하다. 그리고 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장하는데, 그런 경우 대응이 어렵다. 따라서 오브젝트 오류(글로벌 오류)의 경우 `@ScriptAssert` 을 억지로 사용하는 것 보다는 다음과 같이 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장한다.

@PostMapping("/add")
 public String addItem(@Validated @ModelAttribute Item item, BindingResult
 bindingResult, RedirectAttributes redirectAttributes) {
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
         int resultPrice = item.getPrice() * item.getQuantity();
         if (resultPrice < 10000) {
             bindingResult.reject("totalPriceMin", new Object[]{10000,
 resultPrice}, null);
} }
     if (bindingResult.hasErrors()) {
         log.info("errors={}", bindingResult);
         return "validation/v3/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}

 

 

Bean Validation - 수정에 적용

상품 수정에도 빈 검증(Bean Validation)을 적용해보자.
@PostMapping("/{itemId}/edit")
    public String edit2(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {
       

        if (item.getPrice() != null && item.getQuantity()!=null){
            int resultPrice = item.getPrice() * item.getQuantity();
            if( resultPrice < 10000){
                bindingResult.reject("totalPriceMin",new Object[]{10000,resultPrice},null);
            }
        }

        // 검증에 실패하면 다른 입력 폼으로
        if(bindingResult.hasErrors()){
            log.info("error ={}", bindingResult);
            return "/validation/v3/editForm";
        }
        
        itemRepository.update(itemId, item);

        return "redirect:/validation/v3/items/{itemId}";
    }​

- `edit()` : Item 모델 객체에 `@Validated` 를 추가하자.
- 검증 오류가 발생하면 `editForm` 으로 이동하는 코드 추가


`validation/v3/editForm.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;
        }

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

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.updateItem}">상품 수정</h2>
    </div>

    <form action="item.html" th:action th:object="${item}" method="post">

        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each= "err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
        </div>

        <div>
            <label for="id" th:text="#{label.item.id}">상품 ID</label>
            <input type="text" id="id" th:field="*{id}" class="form-control" readonly>
        </div>
        <div>
            <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control">
            <div class="field-error" th:errors="*{itemName}">
                상품명 오류
            </div>
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}" th:errorclass="field-error" class="form-control">
            <div class="field-error" th:errors="*{price}">
                가격 오류
            </div>
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" th:errorclass="field-error" class="form-control">
            <div class="field-error" th: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='item.html'"
                        th:onclick="|location.href='@{/validation/v3/items/{itemId}(itemId=${item.id})}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

-`.field-error` css 추가
- 글로벌 오류 메시지
- 상품명, 가격, 수량 필드에 검증 기능 추가

 

 

 

Bean Validation - 한계

수정시 검증 요구사항

데이터를 등록할 때와 수정할 때는 요구사항이 다를 수 있다.



등록시 기존 요구사항

타입 검증

- 가격, 수량에 문자가 들어가면 검증 오류 처리
필드 검증

- 상품명: 필수, 공백X
- 가격: 1000원 이상, 1백만원 이하
- 수량
: 최대 9999

특정 필드의 범위를 넘어서는 검증
- 가격
* 수량의 합은 10,000원 이상



수정시 요구사항
1)등록시에는 `quantity` 수량을 최대 9999까지 등록할 수 있지만 수정시에는 수량을 무제한으로 변경할 수 있다.
2)등록시에는 `id` 에 값이 없어도 되지만, 수정시에는 id 값이 필수이다.



수정 요구사항 적용

 @Data
public class Item {
@NotNull //수정 요구사항 추가
     private Long id;
     @NotBlank
     private String itemName;
     @NotNull
     @Range(min = 1000, max = 1000000)
     private Integer price;
@NotNull
//@Max(9999) //수정 요구사항 추가 private Integer quantity;
//...
}
id` : `@NotNull` 추가
quantity` : `@Max(9999)` 제거

참고
현재 구조에서는 수정시 `item` 의 `id` 값은 항상 들어있도록 로직이 구성되어 있다. 그래서 검증하지 않아도 된다고 생각할 수 있다. 그런데 HTTP 요청은 언제든지 악의적으로 변경해서 요청할 수 있으므로 서버에서 항상 검증 해야 한다. 예를 들어서 HTTP 요청을 변경해서 `item` 의 `id` 값을 삭제하고 요청할 수도 있다. 따라서 최종 검증 은 서버에서 진행하는 것이 안전한다.


수정을 실행하면정상 동작을 확인할 수 있다.
그런데 수정은 잘 동작하지만 등록에서 문제가 발생한다.
등록시에는 `id` 에 값도 없고, `quantity` 수량 제한 최대 값인 9999도 적용되지 않는 문제가 발생한다.


결과적으로 `item` 은 등록과 수정에서 검증 조건의 충돌이 발생하고, 등록과 수정은 같은 BeanValidation을 적용할 수 없다. 이 문제를 어떻게 해결할 수 있을까?


728x90