-
[김영한 스프링] 28. 검증2 Bean Validation - 수정에 적용 & 한계 & groupsSpring/스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 2023. 9. 7. 22:05
Bean Validation - 수정에 적용
ValidationItemControllerV3 - edit() 변경
@PostMapping("/{itemId}/edit") public String edit(@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("errors={}", 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
- 가격 : 1,000원 이상, 1백만원 이하
- 수량 : 최대 9,999
- 특정 필드의 범위를 넘어서는 검증
- 가격 * 수량의 합은 10,000원 이상
수정 시 요구사항
등록 시에는 quantity 수량을 최대 9,999까지 등록할 수 있지만 수정 시에는 수량을 무제한으로 변경할 수 있다.
등록 시에는 id에 값이 없어도 되지만, 수정 시에는 id 값이 필수이다.
수정 요구사항 적용
수정 시에는 Item에서 id 값이 필수이고, quantity도 무제한으로 적용할 수 있다.
Item
package hello.itemservice.domain.item; import lombok.Data; import org.hibernate.validator.constraints.Range; import org.hibernate.validator.constraints.ScriptAssert; import javax.validation.constraints.Max; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @Data //@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10,000원 넘게 입력해주세요.") 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; public Item() { } public Item(String itemName, Integer price, Integer quantity) { this.itemName = itemName; this.price = price; this.quantity = quantity; } }
수정 요구사항을 적용하기 위해 다음을 적용했다.
id : @NotNull 추가
quantity : @Max(9999) 제거
참고
현재 구조에서는 수정시 item의 id 값은 항상 들어있도록 로직이 구성되어 있다. 그래서 검증하지 않아도 된다고 생각할 수 있다. 그런데 HTTP 요청은 언제든지 악의적으로 변경해서 요청할 수 있으므로 서버에서 항상 검증해야 한다. 예를 들어서 HTTP 요청을 변경해서 item의 id 값을 삭제하고 요청할 수도 있다. 따라서 최종 검증은 서버에서 진행하는 것이 안전한다.실행
정상 동작
상품 등록
수정은 잘 동작하지만 등록에서 문제가 발생한다.
등록 시에는 id에 값도 없고, quantity 수량 제한 최대 값인 9999도 적용되지 않는 문제가 발생한다.
등록 시 화면이 넘어가지 않으면서 다음과 같은 오류를 볼 수 있다.
'id': rejected value [null];
왜냐하면 등록 시에는 id에 값이 없다. 따라서 @NotNull id를 적용한 것 때문에 검증에 실패하고 다시 폼 화면으로 넘어온다. 결국 등록 자체도 불가능하고, 수량 제한도 걸지 못한다.
결과적으로 item은 등록과 수정에서 검증 조건의 충돌이 발생하고, 등록과 수정은 같은 BeanValidation을 적용할 수 없다. 이 문제를 어떻게 해결할 수 있을까?
Bean Validation - groups
동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법을 알아보자.
방법 2가지
- BeanValidation의 groups 기능을 사용한다.
- Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용한다.
BeanValidation groups 기능 사용
이런 문제를 해결하기 위해 Bean Validation은 groups라는 기능을 제공한다.
예를 들어서 등록 시에 검증할 기능과 수정 시에 검증할 기능을 각각 그룹으로 나누어 적용할 수 있다.
저장용 groups 생성
main/java/hello/itemservice/domain/item/SaveCheck 인터페이스 생성
수정용 groups 생성
main/java/hello/itemservice/domain/item/UpdateCheck 인터페이스 생성
Item - groups 적용
package hello.itemservice.domain.item; import lombok.Data; import org.hibernate.validator.constraints.Range; import org.hibernate.validator.constraints.ScriptAssert; import javax.validation.constraints.Max; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @Data //@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10,000원 넘게 입력해주세요.") public class Item { @NotNull(groups = UpdateCheck.class) // 수정 요구사항 추가 private Long id; @NotBlank(groups = {SaveCheck.class, UpdateCheck.class}) private String itemName; @NotNull(groups = {SaveCheck.class, UpdateCheck.class}) @Range(min = 1000, max = 1000000, groups = {SaveCheck.class, UpdateCheck.class}) private Integer price; @NotNull(groups = {SaveCheck.class, UpdateCheck.class}) @Max(value = 9999, groups = SaveCheck.class) // 수정 요구사항 추가 private Integer quantity; public Item() { } public Item(String itemName, Integer price, Integer quantity) { this.itemName = itemName; this.price = price; this.quantity = quantity; } }
ValidationItemControllerV3 - 저장 로직에 SaveCheck Groups 적용
@PostMapping("/add") public String addItem2(@Validated(SaveCheck.class) @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("error = {} ", 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}"; }
- addItem()를 복사해서 addItemV2() 생성, SaveCheck.class 적용
- 기존 addItem()의 @PostMapping("/add") 주석처리
ValidationItemControllerV3 - 수정 로직에 UpdateCheck Groups 적용
@PostMapping("/{itemId}/edit") public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @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("errors={}", bindingResult); return "validation/v3/editForm"; } itemRepository.update(itemId, item); return "redirect:/validation/v3/items/{itemId}"; }
- edit()를 복사해서 editV2() 생성, UpdateCheck.class 적용
- 기존 edit()의 @PostMapping("/{itemId}/edit") 주석처리
참고
@Valid에는 groups를 적용할 수 있는 기능이 없다. 따라서 groups를 사용하려면 @Validated를 사용해야 한다.실행
등록에는 9999 validation 동작, 수정에는 통과
정리
groups 기능을 사용해서 등록과 수정 시에 각각 다르게 검증을 할 수 있었다. 그런데 groups 기능을 사용하니 Item은 물론이고, 전반적으로 복잡도가 올라갔다.
사실 groups 기능은 실제 잘 사용되지는 않는데, 그 이유는 실무에서는 주로 다음에 등장하는 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용하기 때문이다.
출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Spring > 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술' 카테고리의 다른 글
[김영한 스프링] 30. 검증2 Bean Validation - HTTP 메시지 컨버터 (0) 2023.09.07 [김영한 스프링] 29. 검증2 Bean Validation - Form 전송 객체 분리 프로젝트 준비 V4 & 소개 & 개발 (0) 2023.09.07 [김영한 스프링] 27. 검증2 Bean Validation - 스프링 적용 & 에러 코드 & 오브젝트 오류 (0) 2023.09.06 [김영한 스프링] 26. 검증2 Bean Validation - 소개 & 시작 & 프로젝트 준비 V3 (1) 2023.09.05 [김영한 스프링] 25. 검증1 Validation - Validator 분리1, 2 (0) 2023.09.02