Web/Spring

Spring MVC 2 - 검증 (2) BeanValidation

now0204 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 적용불가