-
Spring MVC 2 - 검증 (2) BeanValidationWeb/Spring 2023. 10. 25. 12:47
1. Bean Validation이란
- 검증로직은 대부분 비슷함! 일반적인 로직이다.
- 이러한 검증로직을 모든 프로젝트에 적용할 수 있도록 공통화하고 표준화 한 것이 Bean Validation이다.
- Bean Validation은 기술 표준임 -> 여러 검증 애노테이션과 인터페이스의 모음 [구현체 하이버네이트 Validator]
(ex JAP가 표준기술 -> 구현체 하이버네이트)
2. Bean Validation 시작
- 순수 BeanValidation (쓰려면, 의존관계 gradle에 추가하자)
- 검증 애노테이션
@NotBlank : 빈값 + 공백만 있는 경우
@NotNull: null을 허용하지않는다.
@Range(min,max) : 범위 안의 값
@Max(9999) : 최대 9999까지만 허용
2.1 검증기 테스트
@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()); } }
> 검증기를 생성하고, 생성된 검증기로 검증 -> 추후에는 스프링이 알아서 검증기 생성해서 검증함
> 마치 webDataBinder @Validated처럼..
> 검증결과는 Set에 담김
Set<ConstraintViolation<Item>> violations = validator.validate(item);
> 오류메시지는 하이버네이트에서 만들어줌! (애노테이션에 message=""를 추가해서 바꿀 수 있다)
3. Bean Validation - 스프링에 적용
- 스프링부트에 BeanValidation 라이브러리를 넣으면, Bean Validator를 스프링이 인지하고 통합
- 글로벌 Validator로 등록됨 -> 글로벌로 정의되어 있기 때문에, 검증하려는 매개변수에 @Valid,@Validated만 적용하면됨
- 오류 발생시 FieldError, ObjectError등을 생성해서 BindingResult에 넣어줌
3.1 검증순서
> @ModelAttribute 필드에 타입변환 시도 (실패시 typeMismatch코드로 FieldError 추가)
> Validator 적용
-바인딩에 성공한 필드만 Bean Validation이 적용됨 (정상값이 들어와야 검증하는 의미가 있다)
4. Bean Validation 에러코드
- Bean Validation을 적용한 뒤 생성되는 에러메시지를 보자.
NotBlank.item.itemName NotBlank.itemName NotBlank.java.lang.String NotBlank errors.properties #Bean Validation 추가 NotBlank={0} 공백X Range={0}, {2} ~ {1} 허용 Max={0}, 최대 {1}
-Validation.객체.필드 -> 필드 -> 자료형 -> raw코드 식으로 내려감
- 메시지 코드 매개변수 지정시 {0}은 필드명을 사용, {1},{2}..등은 애노테이션 마다 다름
- default는 하이버네이트가 이미 지정해두었음
5. Bean Validation - 오브젝트 오류
- @SciptAssert()를 사용하면 처리할 수 있다. -> 근데 제약 사항이 많고
-> 검증 해야할 값이 해당 객체의 범위를 넘어서 (DB에서 받아온 값+)
인 경우가 많다.
- 따라서 글로벌 오류는 그냥 직접 자바 코드를 작성하는 편이 좋다.
bindingResult.reject("totalPriceMin", new Object[]{10000,resultPrice}, null);
* 이왕 하려면, BindingResult 사용하자 !!
6. Bean Validation - 한계
- 같은 필드에 대해 수정과 등록에 검증 로직이 다르다면.. 검증할 수 없다.
- @NotNull등 필드에다 적용하기 때문에.. > 검증이 충돌
해결방법
6.1 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;
- 하지만 groups기능은 잘 사용하지않음 복잡..그리고 그룹용 인터페이스 쫌 불필요한듯
6.2 Form전송 객체 분리
- 실무에서 폼 등록시 전달하는 데이터와 Item도메인은 완벽하게 일치하지않는다.
- 실제로는 폼을 통해 전달되는 데이터는 더 복잡하다. 이 중에서 Item에 넣을 수 있는 값만 뽑아서
Form 전용 객체를 따로 만들자.
- 이를 @ModelAttribute로 사용하면 된다. 이후 값을 꺼내서 Item을 생성하자
- 반환과정이 추가되지만, 검증이 쉬워진다.
*현재 등록뷰와 수정뷰가 비슷한데 합칠까?
-> 어설프게 합치면 수많은 분기문을 만나게됨.. 뭔가 분기문이 많으면 뷰를 분리해야하는 신호임
- SaveForm
@Data public class ItemSaveForm { @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1000000) private Integer price; @NotNull @Max(value = 9999) private Integer quantity; }
- UpdateForm
@Data public class ItemUpdateForm { @NotNull private Long id; @NotBlank private String itemName; @NotNull @Range(min = 1000, max = 1000000) private Integer price; //수정에서는 수량은 자유롭게 변경할 수 있다. private Integer quantity; }
@PostMapping("/add") public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) { //특정 필드 예외가 아닌 전체 예외 if (form.getPrice() != null && form.getQuantity() != null) { int resultPrice = form.getPrice() * form.getQuantity(); if (resultPrice < 10000) { bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null); } } if (bindingResult.hasErrors()) { log.info("errors={}", bindingResult); return "validation/v4/addForm"; } //성공 로직 Item item = new Item(); item.setItemName(form.getItemName()); item.setPrice(form.getPrice()); item.setQuantity(form.getQuantity()); Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/validation/v4/items/{itemId}"; }
- 성공로직에 ItemForm을 Item으로 전환하는 과정이 추가됨
- @ModelAttribute에 item으로 이름 넣어줌! 이는 그냥 저장하면 model에 itemSaveForm으로 저장됨..
- 이렇게 저장되면 th:object도 변경해줘야함
7. Bean Validation - HTTP 메시지 컨버터
@Valid와 @Validated는 HttpMessageConverter에도 적용될 수 있다.(@RequestBody)
@PostMapping("/add") public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) { log.info("API 컨트롤러 호출"); if (bindingResult.hasErrors()) { log.info("검증 오류 발생 errors={}", bindingResult); return bindingResult.getAllErrors(); } log.info("성공 로직 실행"); return form; } }
- API의 경우 3가지로 나누어 생각하자
> 성공
>실패: 객체 binding자체 오류
> 검증요청: 객체 변환은 성공했고, Bean Validation 실행
- 타입오류의 경우 이미 JSON을 객체로 변환하는 과정 자체에서 실패 > 검증할 수 없다.
- Bean Validation에 걸리면, 우리가 아는 그 오류코드 생성해낸다.
*bindingResult.getAllErrors()는 ObjectError과 FieldError을 전부 반환한다.
스프링이 이 객체를 JSON으로 변환해서 클라이언트에서 전달했다.
실제 개발에서는 오류 JSON에서 필요한 정보만 뽑아서
다시 별도의 API스펙을 정의하고 객체를 만들어서 반환해야한다고 한다.
7.1 @ModelAttribute vs @RequestBody
HTTP 요청 파라미터를 처리하는 @ModelAttribute는 각 필드 단위로 세밀하게 적용된다.
따라서 특정 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상처리 가능하다.
- 정교한 바인딩, Validator 검증 가능 (바인딩처리에서)
HttpMessageConverter는 필드가 아닌 전체 객체로 적용된다.
@RequestBody는 타입 오류시 바로 예외던짐 -> 컨트롤러 호출안됨, Validator 적용불가
'Web > Spring' 카테고리의 다른 글
Spring MVC(2) - 스프링 타입 컨버터 (0) 2023.11.21 SpringMVC 2 - 서블릿 필터와 인터셉터 (0) 2023.11.15 SpringMVC 2 - 검증 (0) 2023.10.24 Spring MVC 2 - 국제화/메시지 (0) 2023.10.24 스프링 MVC - 웹 페이지 만들기 (0) 2023.10.15