본문 바로가기
SpringBoot

[Spring Boot] 28. REST API (4) - POST method 와 Content-Type

by 청양호박이 2021. 4. 8.

일반적으로 Spring Boot에서 POST method로 개발한 Rest API를 제공한다고 하면, 보통은 json 형태의 data를 body에 넣어서 보내면 정상적으로 동작할 것이라고 생각합니다. 이런 POST 방식으로 제작된 Controller는 보통 아래와 같이 생겼습니다. 

 

[controller]

    @RequestMapping(value = "asdf", method = RequestMethod.POST)
	public ResponseEntity<?> asdf(@RequestBody DTOName dtoName){
		System.out.println(dtoName);
		return null;
	}

특별한 부분은 없고, Back-End에서는 client가 사전에 상호간에 정의된 데이터를 body에 넣어서 요청할 때... 정의된 DTO로 @RequestBody annotation을 추가하여 받아줍니다. 그럼 해당 Rest API를 사용하고자 하는 web application이 axios를 통해서 호출하는 방법도 간단하게 알아보겠습니다. 

 

[axios]

  axios.post('/asdf', {
    first: 'first',
    second: 'second'
  })
  .then(function (res) {
    console.log(res);
  })
  .catch(function (error) {
    console.log(error);
  });

POST method로 Mapping된 URL로 request를 수행합니다. 역시 json 형태로 data를 넣어서 요청을 합니다. 여기서 좀 더 자세하게 알아본다면... @RequestBody annotation의 역할에 집중해 볼 필요가 있습니다. 이 어노테이션은 json형식의 데이터가 들어오면 이를 jackson library를 사용하여 DTO객체에 깔끔하게 넣어주게 됩니다. 

하지만 여기서 간과하면 안되는 부분이 바로 Content-Type입니다. 이렇게 @RequestBody로 자동으로 DTO에 매핑시켜주기 위해서는 이 Content-Type은 반드시 "application/json" 이어야 합니다. 하지만 우리는 axios에서 한번도 Content-Type을 지정해 준 적이 없습니다. 그래도 잘 동작을 하죠?? 그 이유는 default값이 "application/json" 이기 때문입니다. 

 

해당 Request정보를 확인해보면 아래와 같습니다. 

config:
	adapter: ƒ xhrAdapter(config)
	baseURL: "http://localhost:8888/"
	data: "{"first":"first", "second":"second"}"
	headers: {Accept: "application/json, text/plain, */*", 
                Content-Type: "application/json;charset=utf-8"}

Content-Type: "application/json;charset=utf-8" 임을 확인하는 것이 중요합니다. 

 

자 그렇다면... 이번에는 "application/json" 으로 넘어오는 경우, Rest API에서는 @RequestBody annotation 이 없다면 어떤 문제가 생길까요??

INFO 17808 --- [nio-8888-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
INFO 17808 --- [nio-8888-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
INFO 17808 --- [nio-8888-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
RequestBody value : null

아무런 매핑이 되지 않아서 null 이 됩니다. 원래는 아래와 같이 결과가 나오는 것이죠.

INFO 20476 --- [nio-8888-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
INFO 20476 --- [nio-8888-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
INFO 20476 --- [nio-8888-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
RequestBody value : {"first":"first", "second":"second"}

아니... default라면서, 그리고 json형태의 body요청을 가장 많이 사용한다면서 왜이렇게 긴 설명을 할까요?? 눈치채셨을지도 모르지만 꼭 "application/json"을 사용하라는 법은 없기 때문입니다.

 

 

'content-type': 'application/x-www-form-urlencoded'


자 이번에는 Content-Type이 기존에 본 것과 다른 방식에 대해서 알아보겠습니다. 이 방식은 json형식이 아니고 key=value의 쌍으로 이루어진 "application/x-www-form-urlencoded"입니다. "application/json"과 다른점은 생김새 단 한가지입니다.

하지만, 이를 구현하기 위해서는 Back-End는 물론이고 client에서 요청할때도 몇가지 변경이 필요합니다.

 

[Controller]

    @RequestMapping(value = "asdf", method = RequestMethod.POST)
	public ResponseEntity<?> asdf(DTOName dtoName){
		System.out.println(dtoName);
		return null;
	}

controller의 변경된 부분을 찾으셨나요?? @RequestBody annotation이 삭제되었습니다. 해당 어노테이션의 역할은  jackson library를 사용해서 json을 DTO객체에 넣어주었습니다. 하지만 key=value구조에서 동일하게 적용한다면 문제가 생깁니다.

이때 사용하는 것이 @ModelAttribute annotation입니다. 이 경우에는 key=value구조를 DTO객체에 살포시 매핑하여 넣어줍니다. 단, @ModelAttribute를 사용하지 않아도 암묵적으로 적용되기 때문에 생략이 가능합니다. 

 

추가적으로 @ModelAttribute는 매핑하는 DTO에 setter함수가 꼭 있어야 매핑이 됩니다. 따라서 단순히 java.lang.String과 같이 단일값을 가져오는 경우라면 @ModelAttribute를 삭제함으로써 매핑을 수행해야 합니다.  

 

이번에는 axios부문에서의 변경사항을 살펴보겠습니다. 

 

[axios]

const params = new URLSearchParams();

params.append('first', 'first');
params.append('second', 'second');

axios.post('/asdf', params)
.then(function (res) {
  console.log(res);
})
.catch(function (error) {
  console.log(error);
});

key=value의 쌍으로 만들기 위해서는 몇가지 방법이 사용가능합니다. 

 

  1. URLSearchParams class를 통해서 parameter구조 생성
  2. qs.stringify( ) 사용

 

우선 첫번째 사용방법부터 알아보겠습니다. 위의 axios코드와 같이 URLSearchParams 인스턴스를 하나 생성해 줍니다. 그리고 append method를 통해서 원하는 값을 추가해 줍니다. 마지막으로 axios.post로 해당 인스턴스 자체를 넘겨주면 됩니다. 

 

하지만 주의할 점은 URLSearchParams가 모든 browser에서 동작하지 않습니다. 따라서 잘 살펴봐야겠지만 해당 문제는  polyfill로 해결이 가능하다고 합니다. 일단 method가 적용되는 browser정보는 아래와 같습니다. 

 

두번째 방법은 예시코드만 살짝 추가해 보겠습니다. 

const qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));

 

자 이제 구현이 끝났으니, Request정보를 확인해보겠습니다. 

config:
	adapter: ƒ xhrAdapter(config)
	baseURL: "http://localhost:8888/"
	data: "{"first":"first", "second":"second"}"
	headers: {Accept: "application/json, text/plain, */*", 
                Content-Type: "application/x-www-form-urlencoded;charset=utf-8"}

Content-Type: "application/x-www-form-urlencoded;charset=utf-8" 으로 정상적으로 적용이 됬습니다. 그럼 Back-End에서 확인해 보겠습니다. 

 

우선 DTO의 first만 출력했는데... 정상적으로 매핑되어 입력이 되었습니다. 

RequestBody value : first

그렇다면, @RequestBody annotation 이 있다면 어떤 문제가 생길까요?? 아래 보는 것과 같이 그 자체를 String으로 인식하여 전체 params를 하나의 text로 출력해 버립니다.

RequestBody value : first=first&second=second

이렇게 POST의 method의 몇가지 Content-Type에 대한 구현에 대해서 알아보았습니다. 

 

- Ayotera Lab -

댓글