ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC 2 - 검증 (2) BeanValidation
    Web/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
Designed by Tistory.