대체로 검증은 특정 필드 값이 빈 값인지?, 특정 크기를 넘는지, 안넘는지? 와 같은 매우 일반적인 로직이다.
@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 이다!
→ 애노테이션을 넣었으면, 이걸 사용할 수 있는 Validator 가 필요하다!
build.gradle - dependency 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
Item
@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;
}
}
ValidationItemControllerV3
@Slf4j
@Controller
@RequestMapping("/validation/v3/items")
@RequiredArgsConstructor
public class ValidationItemControllerV3 {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "validation/v3/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v3/item";
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v3/addForm";
}
@PostMapping("/add") //실제 저장 로직 실행
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증에 싪패하면 다시 입력 폼으로
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}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, @Validated Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v3/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,@Validated @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
}
→ 기존의 ItemValidator 는 삭제 하고 실행 시켜보자.
실행 결과

잘 된다! 왜??
어떻게 스프링 MVC 는 Bean Validator 를 사용할까?
LocalValidatorFactoryBean을 글로벌 Validator로 등록한다.검증 순서
@ModelAttribute 각각의 필드에 타입 변환 시도
→ 성공 시 다음으로
→ 실패 시 typeMissmatch 로 FieldError 추가.
Validator 적용 (LocalValidatorFactoryBean)
오류 코드가 자동으로 등록된다
어떻게? → 애노테이션 이름으로 등록된다.
@NotBlank
오류 발생시키고 로그를 찍어보면,
codes [NotBlank.item.itemName,NotBlank.itemName,NotBlank.java.lang.String,NotBlank];
@Range
오류 발생시키고 로그를 찍어보면,
codes [Range.item.price,Range.price,Range.java.lang.Integer,Range];
BeanValidation 메시지 찾는 순서
@NotBlank*(*message = "공백은 불가합니다"*)*private String itemName;그동안은 필드에 대한 에러를 검증하였다.
Object 오류는 어떻게 처리할 수 있을까?
@ScriptAssert() 를 사용하면 된다!
검증 희망 Object class 에 애노테이션 @ScriptAssert() 를 붙이고, 원하는 언어, 조건을 작성할 수 있다.
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
@PostMapping("/add") //실제 저장 로직 실행
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
//검증에 싪패하면 다시 입력 폼으로
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}";
}상품 수정에도 Bean Validation을 적용하자.
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,@Validated @ModelAttribute Item item, BindingResult bindingResult) {
//검증에 싪패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("errors = {}", bindingResult);
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
: 데이터를 등록할 때와, 수정할 때는 요구사항이 다를 수 있다!
등록 요구사항과 수정 요구사항이 다르다면?
→ 만약, 아래와 같이 고친다면?
@Data
public class Item {
@NotNull //수정 요구사항 추가
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
//@Max(9999) //수정 요구사항 추가 private Integer quantity;
//...
}
수정시에는 문제가 없다. 하지만, 등록때는 문제가 생긴다!!!
다음의 groups 로 그룹별로 검증방법을 다르게 하여 해결이 가능하다.
interface 2개를 만들고, SaveCheck, UpdateCheck 두개의 인터페이스를 만든다.
Interface
public interface SaveCheck {
}
public interface UpdateCheck {
}
Item.class
@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})
@Max(value = 99999, groups = {UpdateCheck.class})
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
ValidationItemControllerV3
@PostMapping("/add") //실제 저장 로직 실행
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
objectOverThanMinValidation(item, bindingResult);
//검증에 싪패하면 다시 입력 폼으로
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}";
}
@PostMapping("/{itemId}/edit")
public String edit2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
//검증에 싪패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("errors = {}", bindingResult);
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
V3 → V4 copy&paste 하여 준비.
실무에서는 add 와 edit 에서 받는 Item은 다른 경우가 많다 (거의 대부분)
따라서 별도의 객체를 만들어서 전달하고, 예를 들면 ItemSaveForm 이라는 폼을 전달받는 전용 객체를 만들어서 @ModelAttribute로 사용한다. 이를 통해 컨트롤러에서 폼 데이터를 전달받고, 이후 컨트롤러에서 필요한 데이터를 사용하여 Item을 생성한다.
HTML Form -> Item -> Controller -> Item -> RepositoryHTML Form -> ItemSaveForm -> Controller -> Item 생성 -> RepositoryItem 에서의 검증을 모두 제거하자! 이제 Item에서의 검증은 사용하지 않는다.
전송별 객체를 분리한다.
/src/main/java/hello/itemservice/web/validation/form/ItemSaveForm.java
/src/main/java/hello/itemservice/web/validation/form/ItemUpdateForm.java
추가하기
@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;
@NotNull
@Max(value = 99999)
private Integer quantity;
}
ValidationItemControllerV4 수정
@PostMapping("/add") //실제 저장 로직 실행
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm itemSave, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
if (itemSave.getPrice() != null && itemSave.getQuantity() != null) {
int resultPrice = itemSave.getPrice() * itemSave.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(itemSave.getItemName());
item.setPrice(itemSave.getPrice());
item.setQuantity(itemSave.getQuantity());
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
System.out.println(savedItem.getItemName());
System.out.println(savedItem.getPrice());
System.out.println(savedItem.getQuantity());
return "redirect:/validation/v4/items/{itemId}";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm itemUpdate, BindingResult bindingResult) {
//검증에 싪패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("errors = {}", bindingResult);
return "validation/v4/editForm";
}
Item item = new Item();
item.setItemName(itemUpdate.getItemName());
item.setPrice(itemUpdate.getPrice());
item.setQuantity(itemUpdate.getQuantity());
itemRepository.update(itemId, item);
return "redirect:/validation/v4/items/{itemId}";
}
@Valid, @Validated 는 HttpMessageConverter (@RequestBody)에도 적용할 수 있다.
→ @ModelAttribute 는 HTTP 요청 파라미터 - URL 쿼리 스트링, POST form 에서 사용한다.
→ HTTP API 에서는 어떻게 사용해야 할까?
ValidationItemApiController 생성
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("/add")
public Object addItem(@Validated @RequestBody ItemSaveForm saveItem, BindingResult bindingResult) {
log.info("API 컨트롤러 호출");
if (bindingResult.hasErrors()) {
log.info("검증 오류 발생 errors = {}", bindingResult);
return bindingResult.getAllErrors();
}
log.info("성공 로직 실행 ");
return saveItem;
}
}
성공 요청 보낼 경우


“API 컨트롤러 호출” 이라는 로그도 잘 찍힌 것을 알 수 있다.
만약, 실패하는 요청을 보낸다면 어떻게 될까?
실패의 2가지
검증 오류 요청 : JSON을 객체로 생성하는데에 성공 후 검증에서 실패하는 경우.


→“API 컨트롤러 호출” 로그가 찍혔다. 컨트롤러가 실행 되고 검증에서 실패한 Case 이다.
실패 요청 : JSON을 객체로 생성하는 것 자체가 실패하는 경우.


→ “API 컨트롤러 호출” 로그가 없다.
→ 컨트롤러의 호출도 못 한 Case 이다.
@ModelAttribute vs @RequestBody
@ModelAttribute 는 특정 필드에 타입 에러가 발생해도 나머지 필드는 정상 처리할 수 있었다.
하지만 @RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 모하면, 이후 단계 자체가 진행되지 않고, 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.
→ 예외 발생 시 원하는 모양으로 예외를 처리하는 방법은 예외 처리 부분에서 다룰 예정.