본문 바로가기
[ JAVA ]/JAVA Spring

[ Spring Boot ] Bean Validation - HTTP Message Converter

by 환이s 2024. 1. 4.


오늘은 Bean Validation 검증 마지막 챕터인 HTTP Message Converter를 포스팅해 보겠습니다.

이전 포스팅으로 Bean Validation의 개념부터 Form 전송 객체 분리까지 알아봤습니다.

 

 

[ Spring Boot ] Bean Validation

취업 후 회사 설루션 고도화 작업에 투입하면서 여러 가지 코드를 경험하고 기간 내에 끝내야 하는 상황이라서 포스팅을 신경 못쓰고 있었네요.. 오늘부터 포스팅을 다시 시작해보려고 합니다.

drg2524.tistory.com

 

 

[ Spring Boot ] Bean Validation - Form 전송 객체 분리

이전 포스팅에서 Bean Validation의 Groups 기능까지 알아봤습니다. 오늘은 이어서 Form 전송 객체 분리 방식에 대해 포스팅을 해보겠습니다. [ Spring Boot ] Bean Validation 취업 후 회사 설루션 고도화 작업

drg2524.tistory.com

 

(예제 및 소스는 이전 포스팅에 사용된 파일을 기반으로 진행됩니다.)

 


 

BeanValidation(@Valid, @Validated)은 RequestBody에도 적용을 할 수 있습니다.

하지만 몇 가지 주의할 게 있는데 밑에서 다루겠습니다.

 

참고 )

@ModelAttribute는 HTTP 요청 파라미터(URL 쿼리 스트링, POST Form)를 다룰 때 사용한다.

@RequestBody는 HTTP Boty의 데이터를 객체로 변환할 때 사용된다.
주로 API JSON 요청을 다룰 때 사용한다.

 

 

그렇다면 RequestBody에서는 어떻게 다뤄야 할까?

위 참고 사항에서 주고 API를 JSON으로 왔다 갔다 할 때 사용한다고 했다.

 API로 JSON을 다룰 때 Bean Validation을 어떻게 쓰는지 코드로 알아봅시다.


ValidationItemApiController

 

import hello.itemservice.web.validation.form.ItemSaveForm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController //RestController 추가하면 자동으로 @Responsebody가 생성됩니다.
@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;
     }
}

 

 

예제로 사용되는 Controller는 @RestController를 사용해서 Mapping 경로를 설정해 줬습니다.

그다음으로 기존에 사용된 add 경로의 메서드를 가져와서 object를 반환하게 수정하고 여기서 ItemSaveForm에
@requestBody를 추가했습니다.

 

기존에는 form으로 사용되고 있었는데, 이번에는 JSON 형식으로 받을 수 있게 수정했습니다.

추가로 @Validated 검증하는 기능을 넣었기 때문에 BindingResult를 추가해야 합니다.

 

JSON 데이터 요청을 테스트하기 위해 PostMan을 사용합니다.

 

먼저 성공 요청을 보내봅니다.

 

[성공 요청]

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

Postman에서 Body -> raw -> JSON을 선택하고 진행해야 합니다.

 

 

위 박스 데이터 값을 보내면 Controller에서 성공 요청 로그만 출력됩니다.

 

[성공 요청 결과]

API 컨트롤러 호출
성공 로직 실행

 

그러면 이번에는 실패하는 요청을 보내봅니다.

예시로 문자 'A' 같은 걸 넣어보는데, 가격에 aaa 값을 넣어주고 Sand 해보겠습니다.

 

[실패 요청]

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

 

[실패 요청 결과]

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

 

위 실패 요청 결과에서 오류 메시지가 JSON 형식으로 출력되었는데,

결과 메시지는 Spring에서 자동으로 만들어줍니다.

이 부분에서 오류 메시지를 어떻게 만드는지 이런 거는 뒤에 예외 처리에서 다룰 예정이라 생략하겠습니다.

 

다시 본론으로 돌아와서

HttpMessageConverter에서 요청 JSON을 ItemSaveForm 객체로 생성하는데 실패했습니다.

이 경우는 ItemSaveForm 객체를 만들지 못하기 때문에 Controller 자체가 호출되지 않고 그전에 예외가 발생합니다.

 

당연히 호출되지 않기 때문에 Validator도 실행되지 않습니다.

 


 

이번에는 주된 케이스인 검증 오류 요청 테스트를 해보겠습니다.

 

예제로는 가격은 맞는데 수량을 Max 이상으로 입력하고 요청을 보내보겠습니다.

그렇다면 HttpMessageConverter는 성공하지만 검증(Validator)에서 오류가 발생하게 되는데 테스트를 진행해 보겠습니다.

 

[검증 오류 요청]

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

 

수량(quantity)이 10000이면 BeanValidationd @Max(9999)에서 걸리게 됩니다.

 

 @NotNull
 @Max(value = 9999)
 private Integer quantity;

 

 

[검증 오류 결과]

[
     {
         "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"
     }
]

 

Controller에서 검증 오류 발생 시 return bindingResult.getAllErrors();는 ObjectError와 FieldError를 반환합니다.

 

위 결과처럼 Spring이 객체를 JSON으로 변환해서 클라이언트에 전달했습니다. 

 

예제로는 검증 오류 객체들을 그대로 반환했는데, 실무에서는 이 객체들을 그대로 사용하지 말고,

필요한 데이터만 뽑아서 별도의 API 스펙을 정의하고 그에 맞는 객체를 만들어서 반환해야 합니다.

 

[검증 오류 요청 로그]

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 이하여야 합니다]

 

로그를 보면 검증 오류가 정상 수행된 것을 확인할 수 있습니다.

 

TIP)

[ @ModelAttribute vs @RequestBody]

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

HTTPMessageConverter는 @ModelAttribute와 다르게 각각의 필드 단위로 적용되는 것이 아니라,
전체 객체 단위로 적용된다.

따라서 메시지 컨버터의 작동이 성공해서 ItemSaveForm 객체를 만들어야 @Valid, @Validated가 적용된다.

- @ModelAttribute는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.

- @RequestBody는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외를 발생한다. Controller도 호출되지 않고, Validator도 적용할 수 없다. 

마치며

 

오늘까지 BeanValidation 챕터 포스팅이 마무리되었습니다.

 

프로젝트에 투입되면서 기초에 대한 중요성을 몸소 느끼고 있는데,

복습할 수 있는 시간을 가지면서 성장하는 거 같습니다.

 

다음 포스팅은 로그인 처리 챕터로 돌아오겠습니다.

 

위 포스팅은 김영한님의 Spring MVC 2편 - 백엔드 웹 개발 활용  강의를 참고했습니다.

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 - 인프런

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

728x90