지금 까지 우리는 검증 로직을 손수 자바 코드로 구현을 했었다. 물론 스프링이 제공해주는 여러 기능들을 사용해 정말 순수 자바로 짜는 것에 비하면 굉장히 수월하게 만들 수 있었다.
하지만 인간의 욕심은 끝이 없는법,,, 그것마져도 어느 순간 부터는 조금 번거롭게 느껴지기 시작했다. 그래서 새로운 검증기를 도입하였다.
늘 그래왔듯이 우리는 직접 코드를 짜기보다는 어노테이션을 사용함으로써 이전 보다 훨씬 편리하게 검증을 수행할 수 있다.
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
@NotNull(message = "반드시 입력하셔야합니다") //수정 요구사항 추가
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@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}";
}
이제 더이상 WebBinder 도 필요 없고 기존에 만들었던 ItemValidator 도 사용하지 않는다.
단순히 Item 객체의 필드 멤버에 직접 어노테이션들을 등록 함으로써 어노테이션만으로 검증을 수행할 수 있다.
(※ 대신 @Validated 가 꼭 붙어있어야한다)
또한 기본적인 오류메시지를 제공하며, 기본메시지를 따로 설정도 해줄 수 있다.
검증 어노테이션의 종류는 굉장히 많은데
이곳에 방문해보면 굉장히 다양한 어노테이션들을 확인 할 수 있다.
사실 스프링 부트가 자동으로 글로벌 Validator를 등록해준다.
무슨 말이냐면 앞 장에서 우리가 임의로 만든 ItemValidator 를 특정 컨트롤러만이 아닌 모든 컨트롤러에서 사용 할 수 있게 글로벌 Validator 로 등록 하는 방법이 있었다.
바로 @SpringBootApplication 에서 WebMvcConfigurer 를 implementation 해주면 되는데

이 작업을 스프링부트가 알아서 자동으로 해준다는 것이다.
이전과 달리 Bean Validation 을 사용한경우 따로 메시지 코드를 등록하지 않았는데도 자기가 알아서 메시지를 내뱉는다.
왜냐하면 Bean Validation 이 메시지 코드도 자동으로 생성 해주기 때문이다.
Field error in object 'item' on field 'price': rejected value [null]; codes [NotNull.item.price,NotNull.price,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price]; arguments []; default message [price]]; default message [널이어서는 안됩니다] Field error in object 'item' on field 'itemName': rejected value []; codes [NotBlank.item.itemName,NotBlank.itemName,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.itemName,itemName]; arguments []; default message [itemName]]; default message [공백일 수 없습니다] Field error in object 'item' on field 'quantity': rejected value [null]; codes [NotNull.item.quantity,NotNull.quantity,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.quantity,quantity]; arguments []; default message [quantity]]; default message [널이어서는 안됩니다]


이 메시지 코드를 그대로 errors.properties 에 등록 해준다면 우리가 임의로 메시지 내용을 바꿀 수도 있다.
#Bean Validation 추가
#NotBlank.item.itemName=상품 이름을 적어주세요.
#NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
그럼 Bean Validation 으로 오브젝트 오류는 어떻게 잡을 수 있을까?
@ScriptAssert 어노테이션을 사용하면 오브젝트 오류도 해결을 할 수 있다.
하지만 제약조건이 많고 많이 복잡하다. 그렇기 때문에 실무에서는 그냥 오브젝트 오류만 따로 자바코드로 구현하는경우가 많다.
여지껏 "상품 등록" 만 다뤘었는데 이제 한번 "상품 수정" 에도 검증 로직을 추가해보자
수량 : 무제한
id : id값이 반드시 필요함
기존 "상품 등록" 의 경우 수량 의 범위가 9999 개까지 였지만 "상품 수정" 시에는 수량 의 범위가 무제한이 되었다.
또한 등록할 때는 당연히 해당 상품의 id 가 필요가 없지만 수정 시에는 반드시 id 값이 있어야 DB에서 해당 상품을 찾아서 데이터를 수정 할 수 있다.
"등록" 과 "수정" 로직은 같은 Item 객체를 사용한다. 그렇기 때문에 검증 어노테이션을 등록하기가 굉장히 애매해진다.
"등록" 은
quantity의 범위가 정해져있으므로 @Max(9999) 를 등록해줘야하는데"수정" 은 무제한이므로 따로 등록을 해줄 필요가 없다.
또한
"등록" 은
id값이 필요없으므로, 아니 애초에 새로 등록 하는것이기 때문에id가 존재할 수가 없다 허나"수정"은 상품을 찾기위해서 반드시
id가 존재해야하므로@NotNull등록해줘야한다.
이런 경우 충돌이 발생 하게된다.
라는 기능이 있지만 실무에서는 잘 사용하지 않으므로 그냥 내용은 생략. 궁금하면 직접 찾아보슈!!! (넝담 ㅎ)
그럼 도대체 어떻게 저 충돌 문제를 해결 할 수 있을까?
실무에서는 상품을 등록할 때 Item 도메인 객체 "만" 사용 되지 않는다. 대부분 수많은 다른 도메인 객체들의 부가적인 데이터들이 조금씩 꼽싸리 껴서 Item 이랑 같이 넘어오게 된다.
그렇기 때문에 아싸리 "등록" 과 "수정" 별로 각각 별도의 객체를 만들어서
"등록" 시에 필요한 데이터들만을 담는 ItemSaveForm
"수정" 시에 필요한 데이터들만을 담는 ItemUpdateForm 을
따로 따로 만들어 놓은 다음, 클라이언트로 부터 데이터를 전달 받을 때 이것들을 사용한다.
그 다음 컨트롤러에서 각 폼 객체로 부터 데이터들을 전달 받은 다음에 새롭게 Item 객체를 생성하여 이곳에 옮겨 담으면 된다.
등록:
HTML Form -> ItemSaveForm -> Controller -> Service -> Repository -> DB수정 :
HTML Form -> ItemUpdateForm -> Controller -> Service -> Repository -> DB
package hello.itemservice.web.validation.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;
}
package hello.itemservice.web.validation.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 ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
//수정에서는 수량은 자유롭게 변경할 수 있다.
private Integer quantity;
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v4/addForm";
}
@PostMapping("/add")
public String addItem(@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 "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}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "validation/v4/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@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 "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}";
}
@ModelAttribute() 속성에 모델 이름을 "item" 으로 등록 해놨다. 그 이유는 뷰 템플릿에는 현재 th:object= "item"으로 등록 되어있기 때문에 이 코드들을 변경하기 싫어서 위와 같이 설정을 해주었다.
"성공 로직" 의 경우 뷰에서 온 데이터를 ItemSaveForm form 로 저장한다음,
새로운 Item 객체를 하나 생성해서 거기에 옮겨 담는다.
그 다음 repository 에 전달해준다.
"수정" 컨트롤러도 "등록" 과 전부 동일하므로 코드 분석은 생략 하겠다.
본 포스트는
김영한의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 를 보고 정리했습니다.