스프링/4. 스프링 MVC-2

42. Http 메시지(Rest API) 검증

sdafdq 2023. 9. 4. 04:05
@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;
    }
}

@RestController를 쓴다.

@Controller + @ResponseBody이다.

 

즉, 응답을 ResponseBody, HttpMessageConverter를 이용해 해준다.

(자세한 건 https://qwefdg3.tistory.com/270)

 

대충 다시 설명을 하자면,

ResponseBody는 응답을 view같은 템플릿 이름이 아니라 HttpMessageConverter를 사용하면서 반환된다.

 

이거는 Http 메시지 바디에 그대로 넣어주는거다.

 

어떤 형태로 반환할 지는 요청의 Accept 헤더와 반환 타입등을 보고 어떤 컨버터를 쓸 지 결정한다.

 

지금의 경우 Object이니 아마 Json 형태로 반환할 것이다. (만약 요청의 Accept가 */* 전체이거나 application/json일 경우)

 

 

@RequestBody는 Http 메시지 바디에 있는 데이터를 자동형변환 해서 가져오는 것이다.

@ModelAttribute와의 차이점은 먼지 @ModelAttribute는 GET 쿼리파라미터나 form의 POST(이것도 바디를 통해서 오지만)만 받아들일 수 있다.

 

@RequestBody는 바디에 있는 데이터를 받아들이는 것이다. 기본 값은 @ModelAttribute이므로, form의 POST를 제외하고 메시지 바디의 데이터를 받아들이고 싶을 때에는 @RequestBody라고 명시해 줘야 한다.

 

@RequestBody로 받으면 

요청의 Body -> HttpMessageConverter -> 객체 

이렇게 HttpMessageConverter를 거치며 변환이 된다.

(이 부분 참고하면 좋다. https://qwefdg3.tistory.com/267 )

 

여튼 HttpMessageConverter는 컨트롤러의 반환타입이나 인자 타입을 검사, 요청의 경우 대상 헤더의 Content-type을 검사, 응답의 경우 요청 헤더의 Accept를 검사 등을 해서 Json, String 등 어떤 타입으로 바꿀 지 결정

 

HttpMessageConverter는 @RequestBody로 데이터를 받거나 @ResponseBody로 데이터를 보낼 때 거침.

 

 

 

참고로, 이 HttpMessageConverter를 거치는 것들은 @Validated가 있어도 객체를 만들지 못하면 끝이다.

 

HttpMessageConverter를 통해 객체를 만들어야 하는데 

 

원래 그냥 @ModelAttribute경우 타입 등이 안 맞아도 @Validated가 있으면 이어서 컨트롤러가 실행됐지만,

 

@RequestBody나 @ResponseBody경우 아예 멈추고 오류를 반환한다.

 

그러니까 @Validated 이전에 뭘 하는 것인지, 아예 @Validated 자체가 호출이 안된다.

 

HttpMessageConverter가 먼저인가 보다.

 

객체를 만들고 나서, 그 후 검증을 하는 모양이다.

 

객체는 제대로 만들게 끔 해야 한다. 타입이라든지. 제대로 객체가 안만들어지면

{
    "timestamp""2023-09-03T18:55:02.775+00:00",
    "status"400,
    "error""Bad Request",
    "message""",
    "path""/validation/api/items/add"
}

이런 식으로 오류가 나는데, 만약 이렇게 되지 않게끔 하려면 따로 예외처리를 해 줘야 한다.

 

 

그리고 뭐 객체는 제대로 만들어 졌는데, 우리가 만든 검증 로직을(아니 우리가 만든 게 아니고 스프링이 만든 @Validated를 통해)  통과하지 못했다면

 

[
    {
        "codes": [
            "Range.itemSaveForm.price",
            "Range.price",
            "Range.java.lang.Integer",
            "Range"
        ],
        "arguments": [
            {
                "codes": [
                    "itemSaveForm.price",
                    "price"
                ],
                "arguments"null,
                "defaultMessage""price",
                "code""price"
            },
            100000,
            1000
        ],
        "defaultMessage""1000에서 100000 사이여야 합니다",
        "objectName""itemSaveForm",
        "field""price",
        "rejectedValue"10,
        "bindingFailure"false,
        "code""Range"
    },
    {
        "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"100000,
        "bindingFailure"false,
        "code""Max"
    }
]

이런 긴 bindingResult의 모든 에러들을 반환하게 했는데,

실제로 쓸 때는 우리가 API 스펙(HTTP 메시지의 Json 형식을 말하는 듯)을 잘 정의하여 저 에러들에서 필요한 부분만 뽑아 반환토록 해야 한다.

 

 

@ModelAttribute 는 필드단위로 정교하게 바인딩 되지만, @ResponseBody, @RequestBody 등 HttpMessageConverter를 거치는 것 들은 객체를 만들 지 못하면 그냥 끝이다.