Form 전송 객체의 분리
개요
- 이전 글에서 Bean Validation 기능을 통해 폼 객체를 검증하는 방법에 대해 살펴보았다.
- 만약 “
Product 객체
를 등록하는 Form” 과 “Product 객체
를 수정하는 Form” 두가지가 존재할 때, 우리는Product 객체
를 어떻게 다뤄야할까?
공용 Product 객체
사용시 한계점
- 서로 다른 두가지 Form에서 “Bean Validation이 적용된
Product 객체
“를 공용으로 사용한다면, 각 Form에서 서로 다른 검증 조건을 적용할 수 없다. - 또한, 서로 다른 두가지 Form에서 사용하는 객체는 정확히 똑같지 않다.
- 따라서 각 Form 마다 다루는 객체를 따로 분리하여 관리해야한다.
예시 웹 애플리케이션
- 지금부터 예시를 통해 Form 객체를 어떻게 분리하는지 알아보자.
- 본 예시는 상품을 등록하고 수정하는 웹 애플리케이션이다.
- 포스팅 글에서는 자세한 코드는 제공하지 않는다. 전체 소스코드는 아래를 참고하자.
[Point!]
Product
가ProductAddForm
와ProductUpdateForm
로 분리되었다는 사실에 주목하자.
요구사항
- 상품 정보
- 상품 Id
- 상품 이름
- 상품 가격
- 상품을 등록하는 기능
- 상품 id, 상품 이름, 상품 가격을 설정해야한다.
- 상품을 수정하는 기능
- 상품 id는 수정할 수 없다.
- 상품 등록시 검증 조건
- 상품 Id는 비어있을 수 없다.
- 상품 이름은 비어있을 수 없다.
- 상품 가격은 1000 이상이다.
- 상품 수정시 검증 조건
- 상품 이름은 비어있을 수 없다.
- 상품 이름 뒤에 “.Updated” 라는 단어가 붙어야한다.
- 상품 가격은 0 이상이다.
상품 객체: Product
클래스
/**
* 상품도메인
*/
public class Product {
private Long productId;
private String productName;
private Integer productPrice;
// 생성자, getter, setter 생략
}
상품 등록 Form 객체: ProductAddForm
클래스
public class ProductAddForm {
@NotNull
private Long productId;
@NotBlank
private String productName;
@Min(1000)
private Integer productPrice;
//생성자, getter, setter 생략
}
상품 수정 Form 객체: ProductUpdateForm
클래스
public class ProductUpdateForm {
//검증조건이 없다.
private Long productId;
@NotBlank
@Pattern(regexp = "^.*\\.Updated")
private String productName;
@Min(0)
private Integer productPrice;
//생성자, getter, setter 생략
}
컨트롤러: ProductController
클래스
import ...
@Controller
public class ProductController {
private final ProductRepository productRepository;
@Autowired
public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@GetMapping("/product/{productId}")
public String viewProduct(@PathVariable Long productId, Model model) {
model.addAttribute(productRepository.findById(productId));
return "product";
}
@GetMapping("/product/add")
public String viewAddForm(@ModelAttribute Product product) {
return "addProduct";
}
@PostMapping("/product/add")
public String addProduct(@Validated @ModelAttribute("product") ProductAddForm form,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "addProduct";
}
//product 추출
Product product = new Product();
product.setProductId(form.getProductId());
product.setProductName(form.getProductName());
product.setProductPrice(form.getProductPrice());
//product 추가
productRepository.addProduct(product);
//PRG 패턴
redirectAttributes.addAttribute("productId", product.getProductId());
return "redirect:/product/{productId}/update";
}
@GetMapping("/product/{productId}/update")
public String viewUpdateForm(@PathVariable Long productId, Model model) {
Product product = productRepository.findById(productId);
model.addAttribute(product);
return "updateProduct";
}
@PostMapping("/product/{productId}/update")
public String updateProduct(@PathVariable Long productId,
@Validated @ModelAttribute("product") ProductUpdateForm form,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "updateProduct";
}
//product 추출
Product product = new Product();
product.setProductId(productId);
product.setProductName(form.getProductName());
product.setProductPrice(form.getProductPrice());
//product 수정
productRepository.updateProduct(productId, product);
redirectAttributes.addAttribute("productId", product.getProductId());
return "redirect:/product/{productId}";
}
}
상품 등록 뷰 템플릿: addProduct.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<style>
.error-class {
background-color: red;
}
</style>
</head>
<body>
<h1>상품 등록 폼</h1>
<form th:object="${product}" method="post">
상품ID: <input type="text" th:field="*{productId}" placeholder="상품ID" th:errorclass="error-class">
<div th:errors="*{productId}">
상품 ID 오류시, 출력되는 태그
</div> <br/>
상품이름: <input type="text" th:field="*{productName}" placeholder="상품이름" th:errorclass="error-class">
<div th:errors="*{productName}">
상품 이름 오류시, 출력되는 태그
</div> <br/>
상품가격: <input type="text" th:field="*{productPrice}" placeholder="상품가격" th:errorclass="error-class">
<div th:errors="*{productPrice}">
상품 가격 오류시, 출력되는 태그
</div> <br/>
<input type="submit"/>
</form>
</body>
</html>
상품 수정 뷰 템플릿: updateProduct.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<style>
.error-class {
background-color: red;
}
</style>
</head>
<body>
<h1>상품 수정 폼</h1>
<form th:object="${product}" method="post">
상품ID: <input type="text" th:field="*{productId}" placeholder="상품ID" th:errorclass="error-class" disabled>
<div th:errors="*{productId}">
상품 ID 오류시, 출력되는 태그
</div> <br/>
상품이름: <input type="text" th:field="*{productName}" placeholder="상품이름" th:errorclass="error-class">
<div th:errors="*{productName}">
상품 이름 오류시, 출력되는 태그
</div> <br/>
상품가격: <input type="text" th:field="*{productPrice}" placeholder="상품가격" th:errorclass="error-class">
<div th:errors="*{productPrice}">
상품 가격 오류시, 출력되는 태그
</div> <br/>
<input type="submit"/>
</form>
</body>
</html>
상품 정보 뷰 템플릿: product.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<h1>상품 정보</h1>
상품 ID: <p th:text="${product.productId}">상품ID</p>
상품 이름: <p th:text="${product.productName}">상품이름</p>
상품 가격: <p th:text="${product.productPrice}">상품가격</p>
</body>
</html>
설명
- 각 Form(
addProduct.html
,updateProduct.html
)에 따라 사용하는 객체(기존 객체:Product
)가 분리되었다. 그리고 검증 조건도 다르게 설정되었다.ProductAddForm
ProductUpdateForm
- 컨트롤러에서 분리된 Form 객체를 다음과 같이 가져온다.
@Validated @ModelAttribute("product") ProductAddForm form
@Validated @ModelAttribute("product") ProductUpdateForm form
@ModelAttribute("product")
를 통해, Model에 product 라는 이름으로 바인딩한다.
- 본 게시글은 김영한님의 강의를 토대로 정리한 글입니다.
- 더 자세한 내용을 알고 싶으신 분들이 계신다면, 해당 강의를 수강하시는 것을 추천드립니다.