-
[스프링MVC - 2편] 검증 (3)Spring&SpringBoot 2025. 3. 2. 15:37
Bean Validation
검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 Bean Validation이다
Bean Validation을 잘 활용하면, 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다
Item - Bean Validation 애노테이션 적용
public class Item { private Long id; @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1000000) private Integer price; @NotNull @Max(9999) private Integer quantity; //... }
검증 애노테이션
- @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다
- @NotNull : null을 허용하지 않는다
- @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다
- @Max(9999) : 최대 9999까지만 허용한다
Bean Validation 이란?
먼저 Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다
쉽게 이야기해서 검증 애노테이션과 여러 인터페이스의 모음이다
Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다
하이버네이트 Validator 관련 링크
- 공식 사이트 : http://hibernate.org/validator/
- 공식 메뉴얼 : https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/
- 검증 애노테이션 모음 : https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec
Bean Validation 의존관계 추가
// build.gradle implementation 'org.springframework.boot:spring-boot-starter-validation'
BeanValidationTest
public class BeanValidationTest { @Test void beanValidation() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Item item = new Item(); item.setItemName(" "); //공백 item.setPrice(0); item.setQuantity(10000); Set<ConstraintViolation<Item>> violations = validator.validate(item); for (ConstraintViolation<Item> violation : violations) { System.out.println("violation=" + violation); System.out.println("violation.message=" + violation.getMessage()); } } }
Bean Validation - 스프링 적용
ValidationItemControllerV3 코드 수정
@Slf4j @Controller @RequestMapping("/validation/v3/items") @RequiredArgsConstructor public class ValidationItemControllerV3 { private final ItemRepository itemRepository; @GetMapping public String items(Model model) { List<Item> items = itemRepository.findAll(); model.addAttribute("items", items); return "validation/v3/items"; } @GetMapping("/{itemId}") public String item(@PathVariable long itemId, Model model) { Item item = itemRepository.findById(itemId); model.addAttribute("item", item); return "validation/v3/item"; } @GetMapping("/add") public String addForm(Model model) { model.addAttribute("item", new Item()); return "validation/v3/addForm"; } @PostMapping("/add") public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) { 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}"; } @GetMapping("/{itemId}/edit") public String editForm(@PathVariable Long itemId, Model model) { Item item = itemRepository.findById(itemId); model.addAttribute("item", item); return "validation/v3/editForm"; } @PostMapping("/{itemId}/edit") public String edit(@PathVariable Long itemId, @ModelAttribute Item item) { itemRepository.update(itemId, item); return "redirect:/validation/v3/items/{itemId}"; } }
스프링 MVC는 어떻게 Bean Validator를 사용?
스프링 부트가 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합한다
스프링 부트는 자동으로 글로벌 Validator로 등록한다
LocalValidatorFactoryBean을 글로벌 Validator로 등록한다
이 Validator는 @NotNull 같은 애노테이션을 보고 검증을 수행한다
이렇게 글로벌 Validator가 적용되어 있기 때문에, @Valid, @Validated만 적용하면 된다
검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아준다
검증 순서
- @ModelAttribute 각각의 필드에 타입 변환 시도
- 성공하면 다음으로
- 실패하면 typeMismatch FieldError 추가
- Validator 적용
바인딩에 성공한 필드만 Bean Validation 적용
BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다
@ModelAttribute 각각의 필드 타입 변환시도 변환에 성공한 필드만 BeanValidation 적용
- 예) itemName에 문자 "A" 입력 > 타입 변환 성공 > itemName필드에 BeanValidation 적용
- 예) price에 문자 "A" 입력 > "A"를 숫자 타입 변환 시도 실패 > ypeMismatch FieldError 추가 > price 필드는 BeanValidation 적용 X
Bean Validation - 에러 코드
errors.properties
#Bean Validation 추가 NotBlank={0} 공백X Range={0}, {2} ~ {1} 허용 Max={0}, 최대 {1}
BeanValidation 메시지 찾는 순서
- 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
- 애노테이션의 message 속성 사용 > @NotBlank(message = "공백! {0}")
- 라이브러리가 제공하는 기본 값 사용 > 공백일 수 없습니다
Bean Validation - 오브젝트 오류
Bean Validation에서 특정 필드(FieldError)가 아닌 해당 오브젝트 관련 오류(ObjectError)는 어떻게 처리는 @ScriptAssert()를 사용하면 된다
@Data @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >=10000") public class Item { //... }
- 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장하는데, 그런 경우 대응이 어렵다
ValidationItemControllerV3 - 글로벌 오류 추가
@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}"; }
- 따라서 오브젝트 오류(글로벌 오류)의 경우 @ScriptAssert을 억지로 사용하는 것 보다는 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장한다
Bean Validation - 수정에 적용
상품 수정에도 빈 검증(Bean Validation)을 적용
Bean Validation - 한계
수정시 요구사항 발생
- 등록시에는 quantity 수량을 최대 9999까지 등록할 수 있지만 수정시에는 수량을 무제한으로 변경 할수있다
- 등록시에는 id에 값이 없어도 되지만 수정시에는 id 값이 필수이다
- 수정시에는 Item에서 id 값이 필수이고 quantity도 무제한으로 적용할 수 있다
item 수정
@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에 값도 없고, quantity 수량 제한 최대 값인 9999도 적용되지 않는다
- 'id': rejected value [null];
Bean Validation - groups
동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법
- BeanValidation의 groups 기능을 사용한다
- Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm과 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용한다
BeanValidation groups 기능 사용
등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적용할 수 있다
저장용 groups, 수정용 groups 생성
public interface SaveCheck { }
public interface UpdateCheck { }
Item - groups 적용
@Data 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 - 수정 로직에 UpdateCheck Groups 적용
@PostMapping("/{itemId}/edit") public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) { //... }
728x90'Spring&SpringBoot' 카테고리의 다른 글
[스프링MVC - 2편] 로그인 처리1 - 쿠키, 세션 (0) 2025.03.04 [스프링MVC - 2편] 검증 (4) (0) 2025.03.02 [스프링MVC - 2편] 검증 (2) (0) 2025.02.09 [스프링MVC - 2편] 검증 (1) (0) 2025.02.08 [스프링MVC - 2편] 메시지, 국제화 (0) 2025.02.04