앞에서 검증1에서 한 것을 검증 Annotation을 이용하여 간단하게 할 수 있는 방식이다.
@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;
...
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//검증에 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()){
log.info("errors = {}", bindingResult);
return "validation/v3/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
- 이렇게 애노테이션을 붙여서 검증을 할 수 있다. addItem에서 @Validated(@Valid)가 있어야 동작한다.
- 바인딩이 되어야 BeanValidation이 동작한다. 만약 price나 quantity에 문자를 입력하면 BeanValidation이 동작하지 않는다.
검증1에서와 마찬가지로
@NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
@Range
- Range.item.price
- Range.price
- Range.java.lang.Integer
- Range
이런 순서를 가지고 메시지 코드가 생성된다. 이를 이용해 'errors.properties'에 메시지를 정의할 수 있다!
오브젝트 오류의 경우에는
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10000원 넘게 입력해주세요")
public class Item {
...
@ScriptAssert를 이용하여 할 수 있지만 Java 표현식으로
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000,
resultPrice}, null);
}
}
이렇게 작성하는 것이 더 낫다.
BeanValidation - groups : 동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법
등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적는다.
- SaveCheck와 UpdateCheck같이 그룹화할 인터페이스를 각각 만든다.
- 객체 클래스에 아래와 같이 그룹을 적는다.
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)
private Integer price;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Max(value = 9999, groups = SaveCheck.class) //수정 요구사항 추가
private Integer quantity;
...
- 컨트롤러에 아래와 같이 @Validated(value = 그룹)을 적는다. 여기서 value는 생략가능하다.
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes)
...
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult)
...
하지만 이런 방식은 복잡도가 올라가서 잘 사용되지 않는다. 실무에서는 회원가입 시에 약관에 대한 정보도 추가로 받는 등 수 많은 데이터가 날아와서 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용한다. 폼을 분리해서 사용하면 group를 사용할 일이 드물다.
등록용 폼 객체와 수정용 폼 객체 분리
등록용 폼
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
등록 컨트롤러
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//ModelAttribute("item")을 하지않으면 model.addAttribute()에 "itemSaveForm"이라는 이름으로 등록됨. "item"을 적지 않으려면 뷰 템플릿에서 item을 바꿔주어야함.
//특정 필드가 아닌 복합 룰 검증
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}";
}
수정용 폼
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
//수정에서는 수량은 자유롭게 변경할 수 있다.
private Integer quantity;
}
수정 컨트롤러
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @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}";
}
- Form 형태로 받기 때문에 Item객체로 변환해야 한다.
- @ModelAttribute("item") ItemSaveForm form에서 "item"을 따로 적어야 뷰 컨트롤러에 itemSaveForm으로 넘어가지 않도록 할 수 있다.
Bean Validation - HTTP 메시지 컨버터
@ModelAttribute는 HTTP 요청 파라미터(URL쿼리 스트링, POST Form)을 다룰 때 사용한다.
@RequestBody는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용한다.
API의 경우 3가지 경우로 나누어 생각해야 한다.
- 성공 요청: 성공
- 실패 요청: JSON을 객체로 생성하는 것 자체가 실패 (price나 quantity에 문자 넣었을 때)
- 이때는 컨트롤러 요청도 되지 않음.
- 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함
@ModelAttribute vs @RequestBody
- @ModelAttribute는 각각의 필드 단위로 적용되어 한 필드가 오류가 나도 다른 필드들은 정상처리 될 수 있다.
- @RequestBody의 HttpMessageConvertor 단계는 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다. 따라서 메시지 컨버터의 작동이 성공해서 Item이 만들어져야 다음 단계인 컨트롤러가 호출되거나, @Valid나 @Validated가 작동할 수 있다.
- HttpMessageConvertor 단계에서 실패하면 예외로 넘어가는데 이는 예외처리 챕터에서 배운다!
728x90
'Spring' 카테고리의 다른 글
서블릿 필터 (0) | 2024.01.09 |
---|---|
쿠키와 세션 (1) | 2024.01.08 |
검증1-Validation (2) | 2024.01.04 |
메시지, 국제화 (0) | 2024.01.01 |
thymeleaf-1 (0) | 2023.12.31 |