ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [김영한 스프링] 29. 검증2 Bean Validation - Form 전송 객체 분리 프로젝트 준비 V4 & 소개 & 개발
    Spring/스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 2023. 9. 7. 23:18

    Form 전송 객체 분리 - 프로젝트 준비 V4

     

    ValidationItemControllerV4 컨트롤러 생성

    • ValidationItemControllerV3를 ValidationItemControllerV4로 복붙
    • validation/v3를 validation/v4로 모두 변경

     

     

    템플릿 파일 복사

    • v3를 v4로 복붙
    • v4폴더를 선택하고 Ctrl + Shift + R을 눌러 하위 파일의 validation/v3를 validation/v4로 모두 변경

     

     

    실행

     

     

    Form 전송 객체 분리 - 소개

     

    ValidationItemV4Controller

    실무에서는 groups를 잘 사용하지 않는데, 그 이유가 다른 곳에 있다. 바로 등록 시 폼에서 전달하는 데이터가 Item 도메인 객체와 딱 맞지 않기 때문이다.

    소위 "Hello World" 예제에서는 폼에서 전달하는 데이터와 Item 도메인 객체가 딱 맞는다. 하지만 실무에서는 회원 등록 시 회원과 관련된 데이터만 전달받는 것이 아니라, 약관 정보도 추가로 받는 등 Item과 관계없는 수많은 부가 데이터가 넘어온다.

    그래서 보통 Item을 직접 전달받는 것이 아니라, 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 객체를 만들어서 전달한다. 예를 들면 ItemSaveForm이라는 폼을 전달받는 전용 객체를 만들어서 @ModelAttribute로 사용한다. 이것을 통해 컨트롤러에서 폼 데이터를 전달받고, 이후 컨트롤러에서 필요한 데이터를 사용해서 Item을 생성한다.

     

    폼 데이터 전달에 Item 도메인 객체 사용

    • HTML Form -> Item -> Controller -> Item -> Repository
      • 장점: Item 도메인 객체를 컨트롤러, 리포지토리까지 직접 전달해서 중간에 Item을 만드는 과정이 없어서 간단하다.
      • 단점: 간단한 경우에만 적용할 수 있다. 수정 시 검증이 중복될 수 있고, groups를 사용해야 한다.

     

    폼 데이터 전달을 위한 별도의 객체 사용

    • HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository
      • 장점: 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달받을 수 있다. 보통 등록과, 수정용으로 별도의 폼 객체를 만들기 때문에 검증이 중복되지 않는다.
      • 단점: 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가된다.

     

    수정의 경우 등록과 수정은 완전히 다른 데이터가 넘어온다. 생각해 보면 회원 가입 시 다루는 데이터와 수정 시 다루는 데이터는 범위에 차이가 있다. 예를 들면 등록 시에는 로그인id, 주민번호 등등을 받을 수 있지만, 수정 시에는 이런 부분이 빠진다. 그리고 검증 로직도 많이 달라진다. 그래서 ItemUpdateForm이라는 별도의 객체로 데이터를 전달받는 것이 좋다.

     

    Item 도메인 객체를 폼 전달 데이터로 사용하고, 그대로 쭉 넘기면 편리하겠지만, 앞에서 설명한 것과 같이 실무에서는 Item의 데이터만 넘어오는 것이 아니라 무수한 추가 데이터가 넘어온다. 그리고 더 나아가서 Item을 생성하는데 필요한 추가 데이터를 데이터베이스나 다른 곳에서 찾아와야 할 수도 있다.

     

    따라서 이렇게 폼 데이터 전달을 위한 별도의 객체를 사용하고, 등록, 수정용 폼 객체를 나누면 등록, 수정이 완전히 분리되기 때문에 groups를 적용할 일은 드물다.

     

    Q: 이름은 어떻게 지어야 하나요?

    이름은 의미있게 지으면 된다. ItemSave라고 해도 되고, ItemSaveForm, ItemSaveRequest, ItemSaveDto 등으로 사용해도 된다. 중요한 것은 일관성이다.

     

    Q: 등록, 수정용 뷰 템플릿이 비슷한데 합치는게 좋을까요?

    한 페이지에 그러니까 뷰 템플릿 파일을 등록과 수정을 합치는게 좋을지 고민이 될 수 있다. 각각 장단점이 있으므로 고민하는게 좋지만, 어설프게 합치면 수많은 분기문(등록일 때, 수정일 때) 때문에 나중에 유지보수에서 고통을 맛본다.

    이런 어설픈 분기문들이 보이기 시작하면 분리해야 할 신호이다.

     

     

    Form 전송 객체 분리 - 개발

     

    ITEM 원복

    이제 Item의 검증은 사용하지 않으므로 검증 코드를 제거해도 된다.

    package hello.itemservice.domain.item;
    
    import lombok.Data;
    import org.hibernate.validator.constraints.Range;
    import org.hibernate.validator.constraints.ScriptAssert;
    
    import javax.validation.constraints.Max;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    
    @Data
    //@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10,000원 넘게 입력해주세요.")
    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;
        }
    }

    추가했던 애노테이션들 주석 처리

     

     

    ItemSaveForm - ITEM 저장용 폼

    package hello.itemservice.web.validation.form;
    
    import lombok.Data;
    import org.hibernate.validator.constraints.Range;
    
    import javax.validation.constraints.Max;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    
    @Data
    public class ItemSaveForm {
    
        @NotBlank
        private String itemName;
    
        @NotNull
        @Range(min = 1000, max = 1000000)
        private Integer price;
    
        @NotNull
        @Max(value = 9999)
        private Integer quantity;
    }

    main/java/hello/itemservice/web/validation/form/ItemSaveForm 생성

     

     

    ItemUpdateForm - ITEM 수정용 폼

    package hello.itemservice.web.validation.form;
    
    import lombok.Data;
    import org.hibernate.validator.constraints.Range;
    
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    
    @Data
    public class ItemUpdateForm {
    
        @NotNull
        private Long id;
    
        @NotBlank
        private String itemName;
    
        @NotNull
        @Range(min = 1000, max = 1000000)
        private Integer price;
    
        // 수정에서 수량은 자유롭게 변경할 수 있다.
        private Integer quantity;
    }

    main/java/hello/itemservice/web/validation/form/ItemUpdateForm 생성

     

     

    ValidationItemControllerV4 - addItem

        @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("error = {} ", 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}";
        }

    • 기존 코드 제거 : addItem(), addItemV2()
    • 추가 : addItem()

     

    폼 객체 바인딩

    @PostMapping("/add")
    public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form,BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        //...
    }

    Item 대신에 ItemSaveform을 전달받는다. 그리고 @Validated로 검증도 수행하고, BindingResult로 검증 결과도 받는다.

     

    주의

    @ModelAttribute("item")에 item이름을 넣어준 부분을 주의하자. 이것을 넣지 않으면 ItemSaveForm의 경우 규칙에 의해 itemSaveForm이라는 이름으로 MVC Model에 담기게 된다. 이렇게 되면 뷰 템플릿에서 접근하는 th:object이름도 함께 변경해주어야 한다.

     

    폼 객체를 Item으로 변환

    //성공 로직
    Item item = new Item();
    item.setItemName(form.getItemName());
    item.setPrice(form.getPrice());
    item.setQuantity(form.getQuantity());

    Item savedItem = itemRepository.save(item);

    폼 객체의 데이터를 기반으로 Item 객체를 생성한다. 이렇게 폼 객체처럼 중간에 다른 객체가 추가되면 변환하는 과정이 추가된다.

     

     

    실행

     

     

    ValidationItemControllerV4 - edit

        @PostMapping("/{itemId}/edit")
        public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
    
            // 특정 필드가 아닌 복합 룰 검증
            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/editForm";
            }
    
            Item itemParam = new Item();
            itemParam.setItemName(form.getItemName());
            itemParam.setPrice(form.getPrice());
            itemParam.setQuantity(form.getQuantity());
    
            itemRepository.update(itemId, itemParam);
            return "redirect:/validation/v4/items/{itemId}";
        }

    • 기존 코드 제거 : edit(), editV2()
    • 추가 : edit()

     

    수정

    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
        //...
    }

    수정의 경우도 등록과 같다. 그리고 폼 객체를 Item 객체로 변환하는 과정을 거친다.

     

     

    실행

     

     

    정리

    정리Form 전송 객체 분리해서 등록과 수정에 딱 맞는 기능을 구성하고, 검증도 명확히 분리했다.

     

     

    출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

     

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

    웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

    www.inflearn.com

Designed by Tistory.