이전 검증1에서, 검증의 핵심적인 원리 (BindingResult / FieldError, ObjectError / errorCode)에 대한 이해를 했다면,
실제 실무에서는 어떻게 검증처리를 하는지를 알아두는 것이 좋다. 이전의 로직을 매번 작성하기에는 너무 길고 번거롭기 때문이다.
1. Bean Validation 예제 및 기타사항
- Bean Validation 의존성 추가
- 간단 예제
- Bean validation에서 에러메세지 적용하고 싶다면?
- FieldError는 Bean validation으로 처리가능하다면, ObjectError는?
- @Validated 적용원리
2. 실무에서의 검증처리
- Item객체 - 폼 전용 객체 분리 !
- 실제 예제코드
Bean Validation을 위한 의존성추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
Bean validation 간단 예제
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
Bean validation에러 에러 메세지 적용하고 싶으면?
간단함. 앞선 링크에서 정리한대로 bindingResult를 log로 찍어보면 나오는 codes를 기반으로 그대로 작성해주면 됨.
FieldError는 Bean validation으로 처리가능하지만, ObjectError는?
그냥 하던대로, 직접 자바코드를 컨트롤러에 추가해서 작성해주면 됨.
@Validated
적용원리
스프링이 자동으로 @Validated를 보고, 글로벌 validator에 적용된 LocalValidatorFactoryBean으로 에러 검사함..
에러 검사 결과, 에러가 있으면 BindingResult에 FieldError, ObjectError를 추가해준다.
스프링이 @ModelAttribute -> 타입 검사 -> 실패 시, 빈 검증 적용 안함.)
(타입 검사 성공해야, 그 다음에 빈 검증을 적용시켜줌)
그런데 실제 실무에서는 Bean validation을 어떻게 사용할까?
실제로는 데이터를 등록할 때와 수정할 때의 요구사항이 다를 수 있다. 가령 다음 예제와 같다.
등록시 기존 요구사항
타입 검증
가격, 수량에 문자가 들어가면 검증 오류 처리
필드 검증
상품명: 필수, 공백X
가격: 1000원 이상, 1백만원 이하
수량: 최대 9999
특정 필드의 범위를 넘어서는 검증
가격 * 수량의 합은 10,000원 이상
수정시 요구사항
등록시에는 quantity 수량을 최대 9999까지 등록할 수 있지만 수정시에는 수량을 무제한으로 변경할 수 있
다.
등록시에는 id 에 값이 없어도 되지만, 수정시에는 id 값이 필수이다.
그런데 매번 다른 검증을 해주기 위해, Item 도메인을 계속 만드는 건 비효율적인 일이다.
그래서, 실무에서는, Item 도메인과 Form 전송 객체를 분리해준다!!
보통 Item을 직접 전달받는 것이 아니라, 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 객체를 만들어서 전달한다.
아래 코드에서는 ItemSaveForm, ItemUpdateForm 폼 전달받는 전용 객체를 만들어서 @ModelAttribute로 사용한다.
이것을 통해 컨트롤러에서 폼 데이터를 전달받고, 이후 컨트롤러에서 필요한 데이터를 사용해서 Item을 생헝한다.
HTML form -> ItemSaveForm -> Controller
-> Item 생성 -> Repository
코드는 다음과 같다.
ㄴ hello.itemservice
ㄴ domain.item
ㄴ Item.java - 순수 도메인 객체
ㄴ web.validation (패키지)
ㄴ ItemSaveForm.java
ㄴ ItemUpdateForm.java
ㄴ ValidationItemControllerV4의 @PostMapping의 addItem, editV2 메서드 특히 !!
ㄴ repository
// Item.java -- 순수한 도메인 그자체 (검증 빈 로직을 싹 다 뺀..)
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
}
// ItemSaveForm -- Item 저장용 폼
@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 수정용 폼
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
//@Max(value = 9999) -- 수정에서 수량은 자유롭게 변경할 수 있다.
private Integer quantity;
}
// ValidationItemControllerV4
@Controller
@RequestMapping("/validation/v4/items")
@RequiredArgsConstructor
@Slf4j
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";
}
// 여기서는 add, edit에서 @ModelAttribute와 붙은 모델 폼을 각각 따로 만들어 적용!!
// 모델 폼 - ItemSaveForm, ItemUpdateForm 으로 만듦
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
// 여기서 주의!! @ModelAttribute("item") ItemSaveForm form..
// 1. 뷰 템플릿(addForm.html)으로부터 넘어온, item을 ItemSaveForm form으로 바인딩하고..
// 2. 해당 바인딩된 객체를 "item"이름의 모델로 추가해준다.
// 3. @Validated로 검증해주고, 유효성 검사 결과를 BindingResult에 저장한다.
// -- 이때, Bean 어노테이션 결과를 검사해, FieldError와 ObjectError로 추가해준다..
log.info("itemSaveForm : {}", form);
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";
}
//성공 로직 -- !! 이때 폼객체(ItemSaveForm) -> Item으로 변환해주는 과정 필요 !!
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";
}
// 똑같이, Form 전송 객체 분리 ..
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
log.info("ItemUpdateForm : {}", form);
// 특정 필드 예외가 아닌 전체 예외
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("BindingResult - edit : {}", bindingResult);
return "validation/v4/editForm";
}
Item item = new Item();
item.setId(form.getId());
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity());
itemRepository.update(itemId, item);
return "redirect:/validation/v4/items/{itemId}";
}
}