본문 바로가기

Develop/JAVA & Spring

[Spring] Post 요청과 Content-Type의 관계

실무에서 RestAPI를 만들면서 간혹 마주치는 이슈가 있었다.

Client에서 POST 방식으로 요청을 보낼수 있는 API를 만들어서 전달했는데, Client에서 요청시에 아래와 같은 Exception이 발생하며 제대로 동작을 하지 않는 이슈였다.

- HttpMediaTypeNotSupportedException :: 415 status code

{ "timestamp": "2020-03-14T08:57:59.513+0000", "status": 415, "error": "Unsupported Media Type", "message": "Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported", "path": "/add" }

결과적으로는 ContentType을 맞춰서 잘 동작하도록 하곤 했는데, 왜 이런 현상이 생기는지가 궁금하여 원인분석을 해보았다.

Content-Type

RestAPI의 경우 보통 JSON타입으로 요청하고, 요청을 받는다. 그래서 당연히 application/json 타입으로 Content-Type 사용한다고 생각을 하는 경우가 많은데, 자료를 찾다보니 그렇지 않다는걸 알게되었다.

html form 태그를 사용하여 post 방식으로 요청하거나, jQuery의 ajax 등의 요청을 할때 default Content-Type은 'application/json'이 아니라 'application/x-www-form-urlencoded'다.

아래와 같은 코드로 client에서 server로 요청을 보낸다고 해보자.

Content-Type에 따라서 client에서 server로 보내는 데이터의 형식이 달라진다.

const data = { "name": "kim", "age": 29 } axios({ method: 'post', url: 'https://localhost:8080', headers: { 'Content-Type': .... }, data: data }).then((res) => { // handle success }).catch((err) => { // handle error }).then(() => { // always })

- application/x-www-form-urlencoded :

x-www-form-urlencoded의 경우, 아래와 같이 key1=value1&key2=value2 의 형식으로 요청이 가고 있다.

POST / HTTP/1.1 Host: localhost Content-Type: application/x-www-form-urlencoded name=kim&age=29

- application/json :

json의 경우, 아래와 같이 json 형태로 요청이 가는걸 볼수 있다.

POST / HTTP/1.1 Host: localhost Content-Type: application/json { "name":"kim", "age":"29" }

Spring에서 POST 요청 데이터 받기

보통 Spring으로 개발한 RestAPI에서 Post요청을 받을때는 data가 json 형식으로 body에 들어있다고 가정하고 개발을 하게 된다.

따라서 body에서 json 객체를 꺼내어 알맞은 dto로 받기 위해 아래와 같은 코드를 짜게 된다.

@PostMapping(value = "/add") public String postHanlder(@RequestBody Person person) { log.info("person :: {}", person); return person.toString(); }

이때, client에서 header에 content-type으로 'application/json'을 추가해서 보내지 않으면 위에서 말했듯이 415 error가 발생한다.

클라이언트에서 해결하기 위해서는 'content-type'을 'application/json'으로 하여 보내야 한다.

그럼 백엔드에서 해결하기 위해서는 어떻게 해야할까? 실무는 실전이다.. 항상 클라이언트에서 친절하게 해결해주진 않기 때문에 백엔드에서 해결하는 방법을 알고는 있어야 한다.

백엔드에서 처리하는 방법

@RequestBody나 @ModelAttribute, @RequestParam 등의 어노테이션을 사용하게 되면 해당 어노테이션과 매칭되는 '메시지 컨버터'가 AnnotationMethodHandlerAdapter에 의해 등록되게 된다.

1. @RequestBody Person person

model 객체 앞에 @RequestBody 어노테이션을 붙이면, AnnotationMethodHandlerAdapter에 의해 MappingJacksonHttpMessageConverter가 등록되게 된다. MappingJackson2HttpMessageConverter는 HttpMessageConverter의 구현체로 JSON 형식의 데이터가 들어오면 (당연히 content-type은 application/json) 해당 json 데이터를 jackson 라이브러리를 사용하여 model 객체로 변환해주게 된다.

* Content-Type : application/json 이고 json 타입의 데이터

@PostMapping(value = "/add") public String postHanlder(@RequestBody Person person) { log.info("person :: {}", person); return person.toString(); }

2. @RequestBody MulityValueMap<String, String> map

MultiValueMap 과 함께 @RequestBody 어노테이션을 붙이면, AnnotationMethodHandlerAdapter에 의해 FormHttpMessageConverter가 등록되게 된다. FormHttpMessageConverter는 미디어 타입이 application/x-www-form-urlencodede로 정의된 폼 데이터를 주고 받을 때 사용하게 된다.

* Content-Type : application/x-www-form-urlencoded 이고 key1=value1 타입의 데이터

@PostMapping(value = "/add") public String postHanlder(@RequestBody MultiValueMap<String, String> data) { log.info("data :: {}", data); return data.toString(); }

물론 이론상으로는 2번 방법이 가능하지만, FormHttpMessageConverter를 더 효율적으로 사용하려면 아래 3번 방법이 더 낫다.

3. @ModelAttribute Person person

model 객체와 함께 @ModelAttribute를 사용하면 2번과 같이 FormHttpMessageConverter가 등록되어 key=value를 model로 converting 하게 된다. 앞에 아무런 어노테이션도 안붙이고 Person person 으로 선언하면 @ModelAttribute가 암묵적으로 사용된다.

* Content-Type : application/x-www-form-urlencoded 이고 key1=value1 타입의 데이터

@PostMapping(value = "/add") public String postHanlder(Person person) { log.info("data :: {}", data); return data.toString(); }

"저희 부서는 x-www-url-encoded로 제공해주세요"

위에서 정리한것처럼 가장 좋은 방법은 API 명세를 정하여 client들은 application/json 미디어타입과 함께 json 방식으로 요청하도록 강요하는게 가장 깔끔하고 좋다. 하지만, 실무에서는 이러한 요청이 가끔 들어온다. "저희 부서는 이미 모든 요청을 x-www-url-encoded로 하고 있어서, json으로 바꿀수가 없습니다.."와 같은..

다시 말해, 기존에 json 방식으로 요청을 날리던 유저들에게는 그대로 제공하되 추가적으로 x-www-url-encoded 방식도 제공을 해야하는 상황이다.

@PostMapping(value = "/add", consumes = MediaType.APPLICATION_JSON_VALUE) public String postHanlderForJsonRequest(@RequestBody Person person) { log.info(">> json type :: person :: {}", person); return person.toString(); } @PostMapping(value = "/add", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public String postHanlderForFormRequest(Person person) { log.info(">> form type :: person :: {}", person); return person.toString(); }

위와 같이 두개의 method를 만들어주면 된다. 첫번째 method는 json 타입을, 두번째 method는 form-urlencoded 타입을 받게 된다.

존재하지 않는 이미지입니다.

json 타입 요청

x-www-form-urlencoded 타입 요청

정리하자면, 단순히 "POST 방식에서는 @RequestBody를 써야한다"거나, 반대로 "@ModelAttribute는 GET 방식에서만 쓸수있다"는 식의 일차원적인 공부로는 이러한 문제를 마주쳤을때 해결을 하기가 힘들다.

위에서 설명한것처럼 parameter의 signature 타입과 어노테이션이 AnnotationMethodHandlerAdapter를 통해 알맞는 MessageConverter를 등록한다는 이해가 필요하다.

 

출처 - https://blog.naver.com/writer0713/221853596497

 

[Spring] Post 요청과 Content-Type의 관계

도움이 되시면 '광고'를 한번씩 눌러시면 감사하겠습니다 :) 실무에서 RestAPI를 만들면서 ...

blog.naver.com