검증 - Bean Validation(HTTP 메시지 컨버터)

2022. 4. 2. 20:48Spring 기초

들어가기 전에,
@Valid , @Validated 는 HttpMessageConverter ( @RequestBody )에도 적용할 수 있다.
> @RequestBody 는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용한다.

다음과 같은 컨트롤러가 있다.

@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
 @PostMapping("/add")
 public Object addItem(@RequestBody @Validated ItemSaveForm form,
 BindingResult bindingResult) {
 	log.info("API 컨트롤러 호출");
 	if (bindingResult.hasErrors()) {
 		log.info("검증 오류 발생 errors={}", bindingResult);
 		return bindingResult.getAllErrors();
 	}
 	log.info("성공 로직 실행");
    
 	return form;
 }
}

RequestBody로 ItemSaveForm 데이터를 받고, 오류가 발생하면 로그에 찍히도록 하였다.


Postman을 사용해서 테스트를 해보았다. 

POST http://localhost:8080/validation/api/items/add
{"itemName":"hello", "price":1000, "quantity": 10}

API의 경우 3가지 경우를 나누어 생각해야 한다.

성공 요청: 성공
실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패


이런 상황에서 price 값에 숫자가 아닌 문자를 전달해서 실패하게 만들었다.

실패 요청 결과
{
 "timestamp": "2021-04-20T00:00:00.000+00:00",
 "status": 400,
 "error": "Bad Request",
 "message": "",
 "path": "/validation/api/items/add"
}
출력된 로그

[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 객체로 생성하는데 실패한다.
이 경우는 Item 객체를 만들지 못하기 때문에 컨트롤러 자체가 호출되지 않고 그 전에 예외가 발생한다.
물론 Validator도 실행되지 않는다. 이전 포스팅에도 다루었던 내용인데,
바인딩이 되고 나서야 검증하는 것이 의미가 있다. 

 

이번에는 HttpMessageConverter 는 성공하지만 검증(Validator)에서 오류가 발생하는 경우를 확인해보자.

POST http://localhost:8080/validation/api/items/add 
{"itemName":"hello", "price":1000, "quantity": 10000} 

수량( quantity )이 10000 이면 BeanValidation @Max(9999) 에서 걸린다.

컨트롤러의 코드 중

return bindingResult.getAllErrors();


는 ObjectError 와 FieldError 를 반환한다.
스프링이 이 객체를 JSON으로 변환해서 클라이언트에 전달했다. 여기서는 예시로 보여주기 위해서 검증 오류 객체들을 그대로 반환했다. 실제 개발할 때는 이 객체들을 그대로 사용하지 말고, 필요한 데이터만 뽑아서 별도의 API 스펙을 정의하고 그에 맞는 객체를 만들어서 반환해야 한다. 왜냐하면 보다시피 검증 오류 결과가 좀 길다..

 {
 "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

HTTP 요청 파리미터를 처리하는 @ModelAttribute 는 각각의 필드 단위로 세밀하게 적용된다.
그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있었다.

HttpMessageConverter 는 @ModelAttribute 와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다. 따라서 메시지 컨버터의 작동이 성공해서 Item 객체를 만들어야 @Valid , @Validated 가 적용된다.

@ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다. @RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다. 쉽게말하면 JSON은 

{"itemName":"hello", "price":1000, "quantity": 10000}

이 데이터가 한 덩이다. 아까처럼 price에 "A"라는 문자열을 보내서, 
한개의 필드라도 바인딩이 실패하면(객체로 변경하지 못하면) 이후 단계로 진행이 불가능하다. 따라서 Validator를 적용할 수 없다.

'Spring 기초' 카테고리의 다른 글

로그인 처리 - 필터, 인터셉터 (1)  (0) 2022.04.05
@SessionAttribute와 세션정보, 세션 타임아웃 설정  (0) 2022.04.04
검증 - Bean Validation  (0) 2022.04.02
검증 - Validation (2)  (0) 2022.03.31
검증 - Validation(1)  (0) 2022.03.31