Bean Validation
by 키위먹고싶다validation을 코드로 모두 작성하는것은 매우 번거롭다.
이런 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 BeanValidation이다.
이것은 특정 구현체가 아니라 검증 애노테이션과 여러 인터페이스의 모음이다.
어노테이션 하나로 매우 편리하게 검증할 수 있게 한다.
먼저 build.gradle에
implementation 'org.springframework.boot:spring-boot-starter-validation'
추가 해야 한다.
라이브러리에 jakarta.validation-api, hibernate-validator가 추가된다.
@Data
public class Item {
private Long id;
@NotBlank(message = "공백 X")
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(999)
private Integer quantity;
라이브러리 추가 하면 @NotBlank, @NotNull, @Range, @Max등의 어노테이션 사용이 가능하다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증에 실패하면 입력폼으로
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "basic/v3/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/v3/items/{itemId}";
}
@Validated추가
스프링부트는 라이브러를 추가하면 자동으로 BeanValidator를 인지하고 스프링에 통합한다.
LocalValidatorFactoryBean 을 글로벌 Validator로 등록한다. 이 Validator는 @NotNull 같은애노테이션을 보고 검증을 수행한다. 이렇게 글로벌 Validator가 적용되어 있기 때문에, @Valid ,@Validated 만 적용하면 된다.
검증 오류가 발생하면, FieldError , ObjectError 를 생성해서 BindingResult 에 담아준다.
그런데 BeanValidator는 바인딩에 성공한 필드만 BeanValidation을 적용한다.
만약 @ModelAttribute로 각각 필드에 타입 변환을 시도 해서 성공하면 Validator를 적용하고
실패하면 typeMismatch로 FieldError를 추가한다.
갑자기 어리둥절 할 정도로 스프링부트가 알아서 다 해준다??
NotBlank.item.itemName=상품명을 적어주세요
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
BeanValidation이 기본으로 제공하는 오류 메시지를 더 자세하게 변경하고 싶을 경우
@NotBlank를 기반으로 MessageCodesResolver가
NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank
순으로 메시지코드를 만든다.
만약 errors.properties에 추가할 경우 우선순위에 따라 메시지가 출력될 수 있다.
그런데 BeanValidation도 한계점이 있다.
만약 등록 검증 요구사항과 수정 검증 요구사항이 다르다면 어떻게 할까?
Validation의 groups기능을 살펴보자
package hello.itemservice.domain.item;
public interface SaveCheck {
}
public interface UpdateCheck {
}
저장용, 수정용 체크 인터페이스 만들기
@Data
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;
어노테이션 안에 해당 기능이 적용되야 하는 인터페이스 넣어주기
@PostMapping("/add")
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
컨트롤러에 @Validated 에 인터페이스 넣어주기 (등록용)
@PostMapping("/{itemId}/edit")
public String editItemV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult){
수정용
이렇게 하면 등록할 시 검증 따로 , 수정할 시 검증 따로 가 잘 적용된다.
그러나 실무에서 이거 잘 사용안한다.
왜냐하면 실제 상품 등록과 상품 수정을 할때를 생각해보자.
실제 상품 등록하기 위해서는 상품 객체 정보 말고도 회원정보와 부가 정보가 필요하고
또 수정할때의 상품 객체 정보 말고 다른 부가 정보가 필요할때가 많다.
결론은 등록과 수정시 각각 필요한 객체정보가 다를 수 있다.
그래서 등록폼 객체 따로 수정폼 객체 따로 만들어서 사용한다.
package hello.itemservice.web.basic.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;
}
@PostMapping("/add")
public String addItem2(@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 "basic/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:/basic/v4/items/{itemId}";
}
Item대신 컨트롤러에서 ItemSaveForm객체를 받아서 item객체를 만들어서 Repository로 간다.
이때 주의해야 할 점은 @ModelAttribute에 이름을 설정 안하면 규칙에 의해
model.addAttribute.에 itemSaveForm이라는 이름으로 담기는데 그러면 템플릿의 th:object 이름을 item이 아니라 itemSaveForm으로 변경해야 한다. 그래서 @ModelAttribute에 이름을 item으로 넣어줘야한다.
@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 editItemV2(@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 "basic/v4/editForm";
}
Item itemParam = new Item();
itemParam.setItemName(form.getItemName());
itemParam.setPrice(form.getPrice());
itemParam.setQuantity(form.getQuantity());
itemRepository.update(itemId, itemParam);
return "redirect:/basic/v4/items/{itemId}";
}
이것은 수정용
'spring' 카테고리의 다른 글
로그인 - 필터, 인터셉터 (0) | 2022.02.05 |
---|---|
로그인-cookie, session (0) | 2022.02.04 |
Validation (0) | 2022.02.03 |
메시지 국제화 (0) | 2022.02.01 |
타임리프 기본 문법 (0) | 2022.01.28 |
블로그의 정보
kiwi
키위먹고싶다