Bean Validation

dongdong·2022년 4월 26일
0

mvc2

목록 보기
2/2

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;
 //...
}

Bean Validation - 시작

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에서 제공하는 에러 코드 말고 내가 직접 작성하고 싶다면 어떻게 할까?

Bean Validation을 적용하고 bindingResult를 찍어보면
오류 코드가 애노테이션으로 작성된 것을 볼 수 있다.
예)
@NotBlank

  • NotBlank.item.itemName
  • NotBlank.itemName
  • NotBlank.java.lang.String
  • NotBlank

@Range

  • Range.item.price
  • Range.price
  • Range.java.lang.Integer
  • Range

errors.properties 작성

NotBlank={0} 공백X 
Range={0}, {2} ~ {1} 허용

Bean Validation 메시지 찾는 순서
1. 생성된 메시지 코드 순서대로 messageSource 에서 메시지 찾기
2. 애노테이션의 message 속성 사용 @NotBlank(message = "공백! {0}")
3. 라이브러리가 제공하는 기본 값 사용 (공백일 수 없습니다.)

Bean Validation 한계

만약 같은 모델 객체에 상황마다 다른 검증을 적용하고 싶을때
예를들어 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에 담긴다.

Bean Validation - HTTP 메시지 컨버터

@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가 호출 되지 않는다.

  1. 검증 오류 전송
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 가 적용된다.

profile
공부하고 기록하기~

0개의 댓글