Bean Validation 이란?
먼저 Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.
쉽게 이야기해서 검증 애노테이션과 여러 인터페이스의 모음이다. 마치 JPA가 표준 기술이고 그 구현체로
하이버네이트가 있는 것과 같다
아래 코드를 보면 이해하기 쉽다.
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;
//...
}
bulid.gradle 의존 관계 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
1.애노테이션
@NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
@NotNull : null 을 허용하지 않는다.
@Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
@Max(9999) : 최대 9999까지만 허용한다.
2.두가지 인터페이스
javax.validation 으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스이고,
org.hibernate.validator 로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증
기능이다. 실무에서 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용해도 된다.
3.검증 테스트 코드
public class BeanValidationTest {
@Test
void beanValidation() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setItemName(" "); //공백
item.setPrice(0);
item.setQuantity(10000);
Set<ConstraintViolation<Item>> violations = validator.validate(item);
for (ConstraintViolation<Item> violation : violations) {
System.out.println("violation=" + violation);
System.out.println("violation.message=" + violation.getMessage());
}
}
}
실행결과(일부생략)
violation={interpolatedMessage='공백일 수 없습니다', propertyPath=itemName,
rootBeanClass=class hello.itemservice.domain.item.Item,
messageTemplate='{javax.validation.constraints.NotBlank.message}'}
violation.message=공백일 수 없습니다
4. 작동순서
@ModelAttribute -> 각각의 필드 타입 변환시도 -> 변환에 성공한 필드만 BeanValidation 적용
예)price 에 문자 "a" 입력 "a"를 숫자 타입 변환 시도 실패 typeMismatch FieldError 추가
price 필드는 BeanValidation 적용 X
Bean Validation에서 제공하는 에러 코드 말고 내가 직접 작성하고 싶다면 어떻게 할까?
Bean Validation을 적용하고 bindingResult를 찍어보면
오류 코드가 애노테이션으로 작성된 것을 볼 수 있다.
예)
@NotBlank
@Range
errors.properties 작성
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Bean Validation 메시지 찾는 순서
1. 생성된 메시지 코드 순서대로 messageSource 에서 메시지 찾기
2. 애노테이션의 message 속성 사용 @NotBlank(message = "공백! {0}")
3. 라이브러리가 제공하는 기본 값 사용 (공백일 수 없습니다.)
만약 같은 모델 객체에 상황마다 다른 검증을 적용하고 싶을때
예를들어 Item객체를 등록할땐 id값을 검증 할 필요가 없지만
Item객체를 수정 할때는 id값이 필요하다면 어떻게 해야할까??
1. groups
2. dto 만들어서 검증
2번이 주로 사용 되기에 2번에 대해서만 설명
아래 코드를 보면 이해하기 쉽다.
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
}
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
//controller 폼 객체 바인딩
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
//...
//form데이터 -> Item 으로 변환 코드 추가
Item item = new Item();
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity())
Item savedItem = itemRepository.save(item);
}
❗@ModelAttribute로 담을때 name속성을 item이라고 바꿔야한다 안그러면 itemSaveForm으로 model에 담긴다.
@Valid , @Validated 는 HttpMessageConverter ( @RequestBody )에도 적용할 수 있다
@RequestBody는 HTTPbody데이터를 객체로 변환 할때 사용한다. 주로 API JSON 요청을 다룰때 사용한다.
API의 경우 3가지 경우를 나누어 생각해야 한다.
1. 성공 요청: 성공
2. 실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
3. 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함
실패한 요청 전송
POST http://localhost:8080/validation/api/items/add
{"itemName":"hello", "price":"A", "quantity": 10}
price에 문자 'A'를 보냈을때
요청 결과 400에러
{
"timestamp": "2021-04-20T00:00:00.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/validation/api/items/add"
}
실패 요청 로그
w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.http.converter.HttpMessageNotReadableException: JSON parse
error: Cannot deserialize value of type `java.lang.Integer` from String "A":
not a valid Integer value; nested exception is
com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize
value of type `java.lang.Integer` from String "A": not a valid Integer value
at [Source: (PushbackInputStream); line: 1, column: 30] (through reference
chain: hello.itemservice.domain.item.Item["price"])]
HttpMessageConverter에서 요청 JSON을 Item객체로 생성을 실패한것 이경우 객체를 생성하지도 못했기때문에 controller가 호출 되지 않는다.
POST http://localhost:8080/validation/api/items/add
{"itemName":"hello", "price":1000, "quantity": 10000}
검증에서 에러가 났을경우 quantity의 최대가 9999인데 10000을 전송했을때
[
{
"codes": [
"Max.itemSaveForm.quantity",
"Max.quantity",
"Max.java.lang.Integer",
"Max"
],
"arguments": [
{
"codes": [
"itemSaveForm.quantity",
"quantity"
],
"arguments": null,
"defaultMessage": "quantity",
"code": "quantity"
},
9999
],
"defaultMessage": "9999 이하여야 합니다",
"objectName": "itemSaveForm",
"field": "quantity",
"rejectedValue": 10000,
"bindingFailure": false,
"code": "Max"
}
]
API 컨트롤러 호출
검증 오류 발생, errors=org.springframework.validation.BeanPropertyBindingResult: 1
errors
Field error in object 'itemSaveForm' on field 'quantity': rejected value
[99999]; codes
[Max.itemSaveForm.quantity,Max.quantity,Max.java.lang.Integer,Max]; arguments
[org.springframework.context.support.DefaultMessageSourceResolvable: codes
[itemSaveForm.quantity,quantity]; arguments []; default message
[quantity],9999]; default message [9999 이하여야 합니다]
✅@ModelAttribute vs @RequestBody
modelAttribute는 필드단위로 세세하게 적용되서 타입오류가 발생해도 나머지 필드들은 정상 처리가 가능
HttpMessageConverter는 객체단위로 적용된다 Json데이터를 객체로 변환 하지못하면 예외가 발생한다.
반드시 Item객체로 변환 되어야 @Valid,@Validated 가 적용된다.