1. 복습
이전 Chapter에서 컨트롤러와 검증 로직을 분리하고
WebDataBinder에 Validator를 등록하고 @Validate 어노테이션을 사용해, 별도의 검증 로직 호출 없이 동작하는 모습을 보였다.
private final ItemValidator itemValidator;
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
// @Validated: 자동으로 검증기가 실행이 되어, 에러는 BindingResult에 담겨진다.
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
// 검증에 실패하면 다시 입력 폼으로
// BindingResult는 Spring에서 자동으로 View에 넘어가므로, model에 담지 않아도 된다.
if (bindingResult.hasErrors()) {
log.info("errors = {} ", bindingResult); // 로그
return "validation/v2/addForm";
}
// 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// Item의 자식 클래스들도 전부 통과
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors bindingResult) {
Item item = (Item) target;
// 검증 오류 결과를 보관 → BindingResult
// 필드 오류 → new FieldError()
if (!StringUtils.hasText(item.getItemName())) {
// bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.rejectValue("quantity,", "max", new Object[]{9999}, null);
}
// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
}
2. Bean Validation
하지만 이러한 특정 필드에 대한 검증 로직은 단순하긴 하지만 매번 작성하기 번거롭다.
이러한 일반적인 로직을 Spring에서 @Bean Validation으로, 클래스(모델) 내 필드에서 제약 조건을 정의할 수 있다.
Bean Validation
Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.
구현체로는 java에서 지원하는 jakarta.validation과, hibernate에서 지원하는 org.hibernate.validator가 있다.
cf. ORM 과는 관련이 없다.
예를 들어 DB 스키마에 제약 조건을 걸고자 할 때는, @Column(nullable =false) 와 같이 걸어주어야 한다.
1) 라이브러리를 추가하여, 자동으로 LocalValidatorFactoryBean이 글로벌 Validator로 등록한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
cf. 기존 생성한 ItemValidator는 삭제해주어야 한다.
public class ValidationItemControllerV2 {
// private final ItemValidator itemValidator;
// @InitBinder
// public void init(WebDataBinder dataBinder) {
// dataBinder.addValidators(itemValidator);
// }
}
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz); // 검증 객체인지 판단
}
@Override
public void validate(Object target, Errors bindingResult) {
// 검증 로직
}
}
3) 글로벌 Validator이 LocalValidatorFactoryBean이 어노테이션 검증을 실행한다. → Bean Validation
이 때, 검증 오류가 발생하면 FieldError / ObjectError를 생성해서 BindingResult에 담아준다.
@Valid, @Validated : 검증 대상임을 명시
@NotNull, @Min, @Max : 입력값에 대한 검증로직 관리
@Data
public class Item {
@NotNull(groups = UpdateCheck.class)
private Long id;
@NotBlank(message = "공백 X")
private String itemName;
@NotNull(message = "1천 이상 1백만 이하")
@Range(groups = {SaveCheck.class, UpdateCheck.class}, min = 1000, max = 1000000)
private Integer price;
@NotNull(message = "최대 9999")
@Max(9999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
3. Bean Validation - 에러 코드
Bean Validation이 기본으로 제공하는 오류 메시지를 변경하고 싶을 때
message.properties 에 에러 코드에 맞는 메시지를 정의하면 된다.
[ex. 에러 코드]
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
@Range
- Range.item.price
- Range.price
- Range.java.lang.Integer
- Range
4. Bean Validation - 오브젝트 오류
특정 필드(FieldError)가 아닌, 복합 필드(ObjectError) 검증 오류와 같은 경우는
클래스(모델) 내 필드에서 정의하기 어려우므로, 기존 작성 방식대로 직접 자바 코드로 작성하는 것을 권장한다.
// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
동일한 모델 객체를 등록할 때와 수정할 때의 제약 조건을 다르게 설정할 수 있다.
(1) groups
(2) Form 전송 객체 분리
5. 제약 조건 분리 - groups
1) 저장용 / 수정용 groups을 생성한다.
public interface SaveCheck {
}
public interface UpdateCheck {
}
2) 모델 객체에 제약 조건을 적용할 그룹을 적용한다.
@Data
public class Item {
@NotNull(groups = UpdateCheck.class)
private Long id;
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class}, message = "공백 X")
private String itemName;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class}, message = "1천 이상 1백만 이하")
@Range(groups = {SaveCheck.class, UpdateCheck.class}, min = 1000, max = 1000000)
private Integer price;
@NotNull(groups = {SaveCheck.class}, message = "최대 9999")
@Max(9999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
3) 컨트롤러에서 어떤 제약 조건을 적용할 것인지 정의한다.
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
...
}
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
...
}
6. 제약 조건 분리 - Form 전송 객체 분리
1) Item 저장 폼
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
}
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm itemSaveForm, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// ItemSaveForm을 Item 객체로 바꾸는 과정이 필요하다.
Item item = new Item(itemSaveForm.getItemName(), itemSaveForm.getPrice(), itemSaveForm.getQuantity());
// 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v4/items/{itemId}";
}
2) 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;
}
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm itemUpdateForm, BindingResult bindingResult) {
// ItemUpdateForm을 Item 객체로 바꾸는 과정이 필요하다.
Item item = new Item(itemUpdateForm.getItemName(), itemUpdateForm.getPrice(), itemUpdateForm.getQuantity());
itemRepository.update(itemId, item);
return "redirect:/validation/v4/items/{itemId}";
}
7. @Validated - HTTP 메시지 컨버터(@RequestBody)
@ModelAttribute : URL, 쿼리스트링, Post Form
@RequestBody : Http body
지금까지 @ModelAttribute 어노테이션으로 매핑시킬 때의 모델 객체를
@Valid, @Validate와 같은 BeanValidation 방식으로 검증해보았다.
그럼 API 통신을 할 때, JSON 데이터를 Http Message Converter로 매핑시킬 때도 똑같이 검증 로직이 잘 실행될까??
cf. Http Message Converter → https://wch-0625.tistory.com/119
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@GetMapping("/add")
public Object addItem(@Validated @RequestBody ItemSaveForm itemSaveForm, BindingResult bindingResult) {
log.info("API 컨트롤러 호출");
if (bindingResult.hasErrors()) {
log.info("검증 오류 발생 errors-{}", bindingResult);
return bindingResult.getAllErrors();
}
log.info("성공 로직 실행");
return itemSaveForm;
}
}
1) API 검증 경우의 수
- 성공
- 실패 : JSON 객체로 생성하는 것 자체가 실패함
- 검증 오류 : JSON 객체로 생성하는 것은 성공했지만, 검증에서 실패함
2) @ModelAttribute vs. @RequestBody
@ModelAttribute는 각각의 필드 단위로 세밀하게 적용된다.
그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있었다.
@RequestBody는 Http Message Converter가 동작하면서 모델 객체와 매핑을 하게 되는데,
이 때 @ModelAttribute와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다.
따라서 Http Message Converter 동작이 성공해서 모델 객체와 정확히 매핑되어 생성이 되어야 Validater의 검증 로직이 적용된다.
'Spring > MVC 2' 카테고리의 다른 글
7. 로그인 처리2 - 필터, 인터셉터 (0) | 2024.05.04 |
---|---|
6. 로그인 처리1 - 쿠키, 세션 (1) | 2024.05.02 |
4. 검증 1 - Validation (0) | 2024.04.29 |
3. 메시지, 국제화 (0) | 2024.04.29 |
2. 타임리프 - 스프링 통합과 폼 (0) | 2024.04.29 |