Bean Validation은 검증 annotation+ interface의 모음 , 즉 그냥 스펙이다. 구현체는 여러 opensource나 다른 사용 vendor에서 구현이 가능하다. hibernate validator가 존재한다.
package hello.itemservice.domain.item;
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 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;
}
}
NotBlank, NotNull 은 javax.validation 으로 시작하는 annotation 이기 때문에 표준이고 어떤 구현체든지 다 동작을 하는것이다. Range 는 hibernate validator에서만 동작을 한다.
package hello.itemservice.domain.item;
import org.junit.jupiter.api.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
class ItemTest
{
@Test
void beanValidation()
{
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setItemName("");
item.setPrice(0);
item.setQuantity(9999);
Set<ConstraintViolation<Item>> violations = validator.validate(item);
for (ConstraintViolation<Item> violation : violations)
{
System.out.println("violation = " + violation);
System.out.println("violation = " + violation.getMessage());
}
}
}
violation = ConstraintViolationImpl{interpolatedMessage='공백일 수 없습니다', propertyPath=itemName, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
violation = 공백일 수 없습니다
violation = ConstraintViolationImpl{interpolatedMessage='1000에서 1000000 사이여야 합니다', propertyPath=price, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{org.hibernate.validator.constraints.Range.message}'}
violation = 1000에서 1000000 사이여야 합니다
기본적으로 hibernate에서 기본으로 제공되는 메시지들을 출력해준다.
이후에 스프링과 통합되게 되면 factory같은거 쓸 필요 없음!
ConstraintViolation에 validator에서 검증된 오류가 저장된다.
스프링에 적용
기존에 사용했던 ItemValidator (내가 만든것) 을 제거하였다.
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
spring mvc와 validation 이 잘 적용이 되는것을 확인할 수 있는데 @Validated 덕분이다 애는 스프링 표준.
@Valid로 해도 잘 동작한다 애는 javax로 자바 표준.
스프링 부트에 spring boot starter validation 이 dependency가 있으면 자동으로 Bean Validator를 인지하고 스프링에 통합한다.
LocalValidatorFactoryBean을 글로벌 Validator로 등록을 한다. @NotNull 같은것을 보고 검증을 수행한다.
@Validated or @Valid 가 검증하려는 parameter앞에 존재해야 한다.
검증 오류가 발생하면 FieldError , ObjectError를 생성하여 BindingResult에 담아준다.
만약 스스로 개발자가 글로벌 Validator로 등록을 해버리면 Spring이 자동으로 등록이 안된다.
검증 순서는 @ModelAttribute 각각의 필드에 타입 변환 시도를 하고 실패하면 typeMismatch로 FieldError 추가
바인딩에 성공한 필드만 Bean Validation 적용을 한다.
Bean Validation을 적용하고 bindingResult에 등록된 오류코드를 출력해보면 오류 코드가 Annotation 이름으로 등록이 된다. 마치 typeMismatch 처럼 , NotBlank.item.itemName, NotBlank.itemName 등등
다 matching이 안되면 @NotBlank(message = "공백 X") 가 적용이 된다.
MessageCodeResolver 가 똑같은 방식으로 다양한 메세지 코드가 순서대로 생성이된다. error.properties에 추가적으로 바꿔주면 메세지를 등록해줄 수 있다.
Bean Validation 오브젝트오류
Bean Validation , 은 필드에 붙는 annotation 이다.
@ScriptAssert(lang="javascript", script="_this.price * _this.quantity >= 10000",message = "10000원 넘게 입력해주세요.")
안의 조건을 만족시키지 않으면 에러를 출력 시킨다. 하지만 사용하기 좀 힘들다... 주로 자바코드로 작성하는것이 낫다.
이런식으로 작성한후 메소드를 뽑아서 쓰는것이 좋다.
Bean Validation 의 한계
edit에서 만 원하는 요구사항을 추가했다. 예를들어 id NOTNULL 을 추가 했더니 아이템을 등록 할떄 id가 없기 때문에 문제가 발생한다.
하나의 DTO 를 여러 클래스에서 쓰게 되면 각 클래스마다 원하는 Validation이 다를 수 있다.
이때 쓸 수 있는 2가지방법이 존재한다.
1. Validation group기능을 이용하면 된다. 이런식으로 마커 인터페이스를 통해서
package hello.itemservice.domain.item;
public interface SaveCheck {
}
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
@NotBlank
private String itemName;
이런식으로 작성해준다. itemName은 새로 생성할떄나 edit할때나 둘다 필요하기 때문에
public String addItem2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
컨트롤러의 @Validated 안에 넣어주면 해당 SaveCheck 조건만 먹히게 되는것이다. @Valid 에는 그룹기능이 없다. 하지만 복잡도가 올라간다고 해서 잘 사용하지 않는다.
2. Form 전송 객체 분리
예제에서는 폼에서 전달하는 데이터와 Item 도메인 객체가 딱 맞는다. 하지만 실무에서는 회원 등록시 회원과 관련된 데이터만 전달받는 것이 아니라, 약관 정보도 추가로 받는등 수 많은 부가 데이터가 넘어온다. 따라서 ItemSaveForm 이란 객체를 새로 만들어서 받는것이 바람직하다.
@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}";
}
@ModelAttribute("item") 으로 해준이유는 기본값은 ItemSaveForm 이 되어버리기 때문에 item.html에서 th:object 같은애들이 인식을 못하기 때문에 또한 수정하기 귀찮기 때문에 item으로 해당 model 명을 명시적으로 작성한 것이다.
이제 생성과 수정 객체가 분리 되어서 validation 로직이 겹치는 일이 없어졌다.
@ModelAttribute 말고 @RequestBody에도 Validation을 적용가능
@ModelAttribute 는 HTTP 요청 파라미터(URL Query String , Post Form)을 다룰 떄 사용하고, 원본 그대로
@ReqestBody 는 HTTP Body의 데이터를 객체로 변환 할떄 사용한다. API JSON요청을 할때
public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult)
이떄 typeMismatch 에러를 띄우게 이상한 값을 주면 Controller 자체가 호출이 되지 않는다. 왜냐하면 request가 ItemSaveForm 객체로 변환이되고 나서야 Controller 호출이 가능하기 때문이다.
JSON 을 객체로 생성하는것 자체가 실패한 경우다. 이떄는 Validation이고 뭐고 없다.
객체로 바뀌고 나서 Validation 이 적용이 된다.
@ModelAttribute는 바인딩이 실패해도 Controller를 호출 했던거 같은데 왜 @RequestBody는 호출 조차 하지 않을까?
@ModelAttribute는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고 Validator도 적용가능하다.
@RequestBody는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계가 없고 바로 예외를 던진다.
'WEB > Spring MVC 2' 카테고리의 다른 글
Spring MVC 2편 로그인 처리2 - 필터, 인터셉터 (0) | 2021.10.16 |
---|---|
Spring MVC 2편 로그인 처리1 쿠키 세션 (0) | 2021.10.10 |
Spring MVC 2편 검증1 - Validation (0) | 2021.09.05 |
Spring MVC 2편 메시지, 국제화 (0) | 2021.08.21 |
스프링 MVC 2편 - 스프링 통합과 폼 (0) | 2021.08.14 |