ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC - Bean Validation을 위한 Form 전송 객체 분리 (DTO)
    Spring/Spring MVC 2022. 2. 7. 15:43
    반응형

     

     

     

     

    Bean Validation을 위한  Form 전송 객체 분리

     

    Form 전송 객체 분리 이유

     

     일반적으로 데이터를 등록할 때와 수정할 때의 요구사항은 다르기 때문에 등록 폼과 수정 폼의 객체 필드에는 검증 애노테이션을 다르게 적용해야 한다. 각 필드에 검증 애노테이션을 다르게 적용하는 방법은 다음과 같이 2가지 방법이 있다.

     

    1. BeanValidation의 gruops 기능을 사용
    2. Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 객체를
      만들어서 검증에 사용

     

     하지만 groups 기능은 실무에서 잘 사용하지 않는다. 그 이유는 groups 기능 추가로 인한 복잡도 증가와 등록 폼과 수정 폼에서 전달하는 데이터가 Item 도메인 객체와 딱 맞지 않기 때문이다. 예를 들면 회원 등록 폼에서만 다루는 데이터  id, 주민번호, 성별 등은 수정 폼에서는 해당 데이터들을 다루지 않기 때문이다. 또한 폼 전송을 위한 별도의 객체를 만들어서 사용하는 이유는 등록 할때의 검증 로직과 수정 할때의 검증 로직이 달라지는 경우가 있기 때문에 별도의 객체를 만들어서 사용한다. 

     

     실제 업무에서는 폼 전송을 위해 별도의 객체를 만드는 방법을 사용하지만 groups 기능을 사용 방법을 알면 좋기 때문에 어떻게 사용하는지 둘 다 알아보자.


     

    Bean Validation - Group Validation

     

     Group Validation이란 검증 항목들을 그룹화하여 필요에 따라 그룹 단위로 검증을 하는 것이다.  잘 사용하지 않지만 사용 방법은 알아야 하기에 검증하는 방법을 알아보자.

     

     

    인터페이스 생성

     

    • group 단위로 구분을 위해 인터페이스를 사용
    • 인터페이스는 단지 구분을 하는 요도로 쓰이기 때문에 내용은 필요없다
    // 등록 폼 검증을 위한 인터페이스
    public interface SaveCheck {
    }
    
    // 수정 폼 검증을 위한 인터페이스
    public interface UpdateCheck {
    }

     

    검증 객체에 groups 적용

     

    • 해당 폼에서 사용할 검증을 group 단위로 구분하여 적용
    • 검증 애노테이션 groups 파라미터 추가
           ex) groups = UpdateCheck.class,   groups = {SaveCheck.class, UpdateCheck.class}

     

    @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;
        
        ...
    }

     

    검증 객체의 group 선택

     

    • 입력 폼에서 받은 데이터로 바인딩 된 객체를 어떤 group의 검증을 시행할지 지정해준다.
    • @Validated(value = 인터페이스명.class), @Validated(인터페이스명.class) 
            ex) @Validated(UpdateCheck.class)

     

    @Slf4j
    @Controller
    @RequestMapping("/validation/v3/items")
    @RequiredArgsConstructor
    public class ValidationItemControllerV3 {
    
    	...
        
        @PostMapping("/add")
        public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        
        	...
        }
        
        @PostMapping("/{itemId}/edit")
        public String edit2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
       
    		...
        }
    }

     

     이와같이 groups 기능을 사용하면 각 검증 객체를 각각 다르게 검증할 수 있다. 하지만 위 예시 코드에서 볼 수 있듯이 Item 클래스의 복잡도가 증가한다. 지금은 Item 클래스가 몇개 없는 필드만 있어 많이 복잡해 보이진 않지만 실제 실무에서는 위 예시 코드보다 더 많은 필드들이 있어 groups 기능을 사용하면 복잡도가 많이 증가 할 것이다. 때문에 groups 기능은 잘 사용하지 않는다.


     

     

    Bean Validation - Form 전송 객체 분리

     

    폼 데이터 전달에 도메인 객체 사용과 별도의 객체 사용의 차이점

     

    • 도메인 객체 사용
          - 데이터 전달 : HTML Form -> Item -> Controller -> Item -> Repository
          - 장점 : 도메인 객체(Item)이 컨트롤러, 리포지토리까지 직접 전달하여 간단
          - 단점 : 간단한 경우에만 적용이 가능하다.
                     groups를 사용해야 한다.
    • 별도의 객체 사용
          - 데이터 전달 : HTML Form -> ItemSaveForm-> Controller -> Item 생성 -> Repository
          - 장점 : 전송하는 폼 데이터에 맞게 별도의 객체(ItemSaveForm)를 사용해서 전달 받을 수 있다.
                    등록, 수정용 폼 객체를 각각 만들기 때문에 검증이 중복되지 않는다.
          - 단점 : 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가된다.

     

     

    Form 전송 객체 분리를 위한 클래스 생성 (DTO)

     

    • 전송하는 폼 데이터에 맞게 별도의 객체를 사용하기 때문에 기존 도메인 클래스(Item)에
      붙어있는 애노테이션은 다 제거해줘도 된다.

    • 별도의 객체를 사용하기 위한 클래스를 만들어 준다.

     

    Item 도매인 클래스

    @Data
    public class Item {
    
        private Long id;
        private String itemName;
        private Integer price;
        private Integer quantity;
        
        ...
    }

    검증에 사용되었던 애노테이션 제거

     

    ItemSaveForm 별도의 클래스

    @Data
    public class ItemSaveForm {
    
        @NotBlank
        private String itemName;
    
        @NotNull
        @Range(min = 1000, max = 1000000)
        private Integer price;
    
        @NotNull
        @Max(value = 9999)
        private Integer quantity;
    }

    등록 폼 데이터에 맞는 클래스 생성

     

    ItemUpdateForm 별도의 클래스

    @Data
    public class ItemUpdateForm {
    
        @NotNull
        private Long id;
    
        @NotBlank
        private String itemName;
    
        @NotNull
        @Range(min = 1000, max = 1000000)
        private Integer price;
    
        //수정에서는 수량 검증을 안하도록 수정함
        private Integer quantity;
    }

    수정 폼 데이터에 맞는 클래스 생성


     

    별개의 객체를 이용하여 검증

     

    • 각 컨트롤러에서 폼 데이터에 맞는 별도의 객체를 주입받아 사용
    • 도메인 객체를 생성하여 데이터를 전달하고 Repository에 접근해야 한다

     

    @Slf4j
    @Controller
    @RequestMapping("/validation/v4/items")
    @RequiredArgsConstructor
    public class ValidationItemControllerV4 {
    
    	...
        
        @PostMapping("/add")
        public String addItem(@Validated @ModelAttribute ItemSaveForm itemSaveForm, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    
            ...
            
            Item item = new Item();
            item.setItemName(itemSaveForm.getItemName());
            item.setPrice(itemSaveForm.getPrice());
            item.setQuantity(itemSaveForm.getQuantity());
    
            Item savedItem = itemRepository.save(item);
            redirectAttributes.addAttribute("itemId", savedItem.getId());
            redirectAttributes.addAttribute("status", true);
            return "redirect:/validation/v4/items/{itemId}";
        }
        
        @PostMapping("/{itemId}/edit")
        public String edit(@PathVariable Long itemId, @Validated @ModelAttribute ItemUpdateForm itemUpdateForm, BindingResult bindingResult) {
    
            ...
            
            Item itemParam = new Item();
            itemParam.setItemName(itemUpdateForm.getItemName());
            itemParam.setPrice(itemUpdateForm.getPrice());
            itemParam.setQuantity(itemUpdateForm.getQuantity());
            
            itemRepository.update(itemId, itemParam);
            return "redirect:/validation/v4/items/{itemId}";
        }

     

     참고로 각 form에서 사용하는 th:object 또한 각각 itemSaveForm, itemUpdateForm 으로 수정해야 한다.

    또한 에러 코드도 달라지기 때문에 에러 메시지 파일에 새로 작성해 줘야한다.


     

    참조 강의

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 학습 페이지 (inflearn.com)

     

    반응형

    댓글

Designed by Tistory.