본문 바로가기
[ JAVA ]/JAVA Spring

[ Spring Boot ] Bean Validation - Form 전송 객체 분리

by 환이s 2024. 1. 2.
728x90


이전 포스팅에서 Bean Validation의 Groups 기능까지 알아봤습니다.

오늘은 이어서 Form 전송 객체 분리 방식에 대해 포스팅을 해보겠습니다.

 

[ Spring Boot ] Bean Validation

취업 후 회사 설루션 고도화 작업에 투입하면서 여러 가지 코드를 경험하고 기간 내에 끝내야 하는 상황이라서 포스팅을 신경 못쓰고 있었네요.. 오늘부터 포스팅을 다시 시작해보려고 합니다.

drg2524.tistory.com

 

(Form 전송 객체 분리에 나오는 예제 및 소스는 이전 포스팅에 사용된 파일을 기반으로 진행됩니다.)

 


 

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

 

조금 더 풀어보자면 회원 등록 시 회원과 관련된 데이터만 전달받는 것이 아니라, 약관 정보도 추가로 받는 등, Item과 관계없는 수많은 부가 데이터가 넘어옵니다.

 

그래서 보통 Item을 직접 전달받는 것이 아니라, 복잡한 폼의 데이터를 Controller까지 전달할 별도의 객체를 만들어서 전달하는데, 예를 들면 ItemSaveForm이라는 폼을 전달받는 전용 객체를 만들어서  @ModelAttribute로 사용합니다.

 

이것을 통해 Controller에서 폼 데이터를 전달 받고, 이후 Controller에서 필요한 데이터를 사용해서 Item을 생성합니다.

 

 

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

 

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

 

 

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

 

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

 

Update 경우 등록과 수정은 완전히 다른 데이터가 넘어옵니다.

생각해보면 회원 가입 시 다루는 데이터와 수정 시 다루는 데이터는 범위에 차이가 있습니다.

 

예를 들면 등록 시에는 Login ID, 주민번호 등등을 받을 수 있지만, 수정 시에는 이런 부분이 제외됩니다.

그리고 검증 로직도 많이 달라지는데, 그래서 ItemUpdateForm이라는 별도의 객체로 데이터를 전달받는 것이 좋습니다.

 

Item 도메인 객체를 폼 전달 데이터로 사용하고, 그대로 쭉 넘기면 편리하겠지만...

앞에서 설명한 것과 같이 실무에서는 Item의 데이터만 넘어오는 것이 아니라 무수한 추가 데이터가 넘어온다.
그리고 더 나아가서 Item을 생성하는데 필요한 추가 데이터를 데이터베이스나 다른 곳에서 찾아와야 할 수도 있다.

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

 

 

그럼 이제 코드로 알아봅시다.

 


Form 전송 객체 분리 - 개발

 

이전 포스팅에서 사용된 Item 파일에서 이번 포스팅에서는 검증이 사용하지 않으므로 검증 코드를 제거해도 됩니다.

 

  • Item
@Data
public class Item {

     private Long id;
     private String itemName;
     private Integer price;
     private Integer quantity;
     
}

 

 

  • ItemSaveForm - Item 저장용 폼
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;
}

 

 

  • ItemUpdateForm - Item 수정용 폼
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;
}

 

다음으로는 등록, 수정용 폼 객체를 사용하도록 Controller를 수정합니다.

 

  • Controller
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import hello.itemservice.web.validation.form.ItemSaveForm;
import hello.itemservice.web.validation.form.ItemUpdateForm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;

@Slf4j
@Controller
@RequestMapping("/validation/v4/items")
@RequiredArgsConstructor
public class ValidationItemControllerV4 {

 private final ItemRepository itemRepository;
 
 @GetMapping
 public String items(Model model) {
 
	 List<Item> items = itemRepository.findAll();
	 model.addAttribute("items", items);
	 
 return "validation/v4/items";
 
 }
 
 @GetMapping("/{itemId}")
 public String item(@PathVariable long itemId, Model model) {
 
	 Item item = itemRepository.findById(itemId);
	 model.addAttribute("item", item);
	 
 return "validation/v4/item";
 
 }
 
 @GetMapping("/add")
 public String addForm(Model model) {
 
	model.addAttribute("item", new Item());	
 
 return "validation/v4/addForm";
 
 }
 
 @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}";
	 
 }
 
 @GetMapping("/{itemId}/edit")
 public String editForm(@PathVariable Long itemId, Model model) {
 
	 Item item = itemRepository.findById(itemId);
	 model.addAttribute("item", item);
	 
	 return "validation/v4/editForm";
 }
 
@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}";
	 }
}

 

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

추가된 addItem(), eidt() 메서드가 이전 방식과 다른 점에 대해 알아보자면

 

  • 폼 객체 바인딩
@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 객체를 생성합니다.

이렇게 폼 객체처럼 중간에 다른 객체가 추가되면 변환하는 과정이 추가됩니다.

 

 

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

 

수정의 경우도 등록과 같습니다.

그리고 폼 객체를 Item 객체로 변환하는 과정을 거칩니다.

 


마치며

 

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

 

해당 소스는 깃허브에서 확인이 가능하며 동작 시 정상적으로 실행됩니다.

다음 포스팅은 HTTP 메시지 컨버터로 찾아뵙겠습니다.

 

위 포스팅은 김영한님의 Spring MVC 2편 - 백엔드 웹 개발 활용  강의를 참고했습니다.

 

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

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

www.inflearn.com

 

 

GitHub - KiHwanY/Spring_MVC_Middle_Validation: 김영한님의 스프링 MVC 2편 강의 - Spring_MVC_Middle_Validation

김영한님의 스프링 MVC 2편 강의 - Spring_MVC_Middle_Validation - GitHub - KiHwanY/Spring_MVC_Middle_Validation: 김영한님의 스프링 MVC 2편 강의 - Spring_MVC_Middle_Validation

github.com

 

728x90