
검증 애노테이션과 여러 인터페이스를 모은 기술 표준으로, 여러 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고 표준화한 것 이다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
Bean Validation을 사용하려면 bulid.gradle에 의존관계를 추가해주어야한다.@NotBlank
private String itemName;
@NotNull
@Range(min=1000, max=1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
@NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.@NotNull : null을 허용하지 않는다.@Range(min=1000, max=1000000) : 범위 안의 값이여야 한다.@Max(9999) : 최대 9999까지만 허용한다.📌참고
javax.validation.constraints.NotNull
org.hibernate.validator.constraints.Range
@NotNull,@NotBlank의 경우javax가 제공하는 표준 인터페이스이므로 어떤 구현체에서도 동작한다.
@Range의 경우 하이버네이트 validator 구현체를 사용할 떄만 제공되는 검증 기능이다.
하지만 실무에서는 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용해도 된다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {...}
@Valid 또는 @Validated만 적용하면 검증기가 실행된다.📌
@Valid와@Validated
@Valid: 자바 표준 검증 애노테이션
@Validated: 스프링 전용 검증 애노테이션
현재로써는 둘 중 아무거나 사용해도 동일하게 작동하지만, 뒤에 나올groups기능을 사용하려면@Validated를 써야한다.
@ModelAttribute가 각각의 필드에 타입 변환을 시도typeMismatch로 FieldError추가Validator 적용바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다.
즉, 타입 변환에 성공해서 바인딩에 성공한 필드여야만 BeanValidation 적용이 의미가 있다.
itmeName에 문자 "A" 입력 -> 타입 변환 성공 ->itemName필드에 BeanValidation 적용price에 문자 "A" 입력 -> 숫자 타입 변환 시도 실패 ->typeMismatch FieldError추가 ->price필드에 BeanValidation 적용 불가
bindingResult에 등록된 검증 오류 코드는 MessageCodesResolver에 의해 생성된다.#오류 코드
@NotBlank
NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank
@Range
Range.item.price
Range.price
Range.java.lang.Integer
Range
#Bean Validation 추가
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
{0} : 필드명{1},{2} : 각 애노테이션마다 다름@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
📌BeanValidation 메시지 찾는 순서
1. 생성된 메시지 코드 순서대로messageSource에서 찾기
2. 애노테이션의message속성 사용
3. 라이브러리가 제공하는 기본 값 사용
특정 필드가 아닌 해당 오브젝트 관련 오류는 @ScriptAssert()를 사용하면 된다.
@Data
@ScriptAssert(lang="javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 만원이 넘어야합니다.")
public class Item{
...
}
@ScriptAssert()는 제약이 많고 복잡하며, 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우도 종종 등장하는데, 그런 경우 대응이 어렵다.
따라서 오브젝트 오류의 경우 @ScriptAssert()를 사용하기 보다 이전과 같이 오브젝트 오류 관련 부분만 자바코드로 작성하는 것을 권장한다.
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
데이터 등록 시와 수정 시 요구사항이 다를 수 있다.
quantity수량을 최대 9999까지 등록할 수 있지만, 수정 시에는 수량을 무제한으로 변경할 수 있다.id에 값이 없어도 되지만 수정시에는 id 값이 필수이다.이 경우, 수정기능은 잘 작동하지만 등록시에는 id값이 없고
quantity최대 값인 9999도 적용되지 않는다.
이러한 문제점을 해결 할 수 있는 방법은 크게 두가지이다.
Groups를 사용하면 등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적용할 수 있다.package hello.itemservice.domain.item;
public interface SaveCheck {
}
package hello.itemservice.domain.item;
public interface UpdateCheck {
}
@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;
@PostMapping("/add")
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {...}
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(value = UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {...}
📌참고
@Valid에는groups를 적용할 수 있는 기능이 없다.
groups를 사용하려면@Validated를 사용해야 한다.
groups를 사용하면 등록, 수정 시에 각각 다르게 검증을 할 수 있다.
하지만 전반적으로 복잡도가 올라갔으며, 실무에서는 groups 기능은 잘 사용하지 않고 다음에 등장할 Form 전송 객체 분리를 주로 사용한다.
등록과 수정을 각각 처리하는 전용 객체를 @ModelAttribute로 사용해 컨트롤러에서 데이터를 전달받고, 필요한 데이터를 사용해 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;
}
@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("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
...
//성공로직
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}";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
...
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}";
}
Item대신 각각 ItemSaveForm, ItemUpdateForm을 전달받는다.Item객체를 생성한다. 폼 객체처럼 중간에 다른 객체가 추가되면 변환하는 과정이 추가된다.📌주의
@ModelAttribute에item이름을 넣어주지 않으면 자동으로itemSaveForm이라는 이름으로Model에 담기게 된다.
이렇게 되면 뷰 템플릿에서 접근하는th:object도 함께 변경해 주어야한다.