MessageConverter

HTTP 프로토콜에서 메시지는 문자열을 통해 전송 됩니다.

MessageConvert는 HTTP 통신에서 일반 문자열이 아닌 XML 이나 JSON으로 통신하기 위해 사용됩니다.

주로 Ajax 통신을 할 때 사용되는데, 이 글도 Ajax로 JSON을 전달하는 범위 내에서 말씀드릴 것입니다.

우선 왜 JSON을 전달하는지 그 이유를 알아보도록 하겠습니다.



대부분의 브라우저에는 XMLHttpRequest라는 내장 브라우저가 존재합니다.

XMLHttpReqeust는 화면이 없는 브라우저이며, 초기에는 이름 그대로 XML 방식으로 HTTP 통신만 담당했습니다.

브라우저 화면이 없으므로 HTML 문서를 받을 필요가 없고, 따라서 통신하는데 필요한 데이터를 XML 형식으로 주고 받는 역할을 하면서 Ajax 통신이 가능하게 되었습니다.


그런데 브라우저에서 XML을 조작하는 것은 JS인데 굳이 XML으로 전달하여 다시 JS로 파싱 할 필요가 없음을 깨닫게 되었습니다.

그래서 JS 객체 형식으로 전달하는 JSON을 사용하게 되었죠.


정리하면, 브라우저 내에 XMLHttpRequest라는 내장 브라우저가 존재하여 Ajax 통신이 가능한데, XML로 데이터를 전송하는 것은 비효율적이므로 JSON으로 데이터를 주고 받는다는 것입니다.

그러면 이제 스프링에서 Ajax 통신을 할 때 String으로 데이터를 전달하지 않고, JSON 형태로 데이터를 전달하는 MessageConverter에 대해 알아보도록 하겠습니다.





@ResponseBody와 StringMessageConverter

우선 Ajax를 알아보기 전에 @ResponseBody 어노테이션이 무엇인지 알아야 합니다.


기본적인 환경 설정은 생략하고 컨트롤러 파일을 생성하여 아래와 같이 작성하겠습니다.

JoinController

@Controller
public class JoinController {

	@ResponseBody
	@RequestMapping("/join")
	public String join() {
		return "ㅎㅇㅎㅇ";
	}
}

저의 프로젝트 폴더 이름이 messageConverterTest 이므로 URL에 http://localhost:8080/messageConverterTest/join 를 요청하면 결과를 얻을 수 있습니다.

지금까지는 return 값으로 뷰 페이지의 이름을 작성했었습니다.

그런데 이번에는 "ㅎㅇㅎㅇ"라는 한글을 입력했고, 메서드 위에 어노테이션으로 @ResponseBody를 명시했습니다.


@ResponseBody는 뷰 페이지를 응답하지 않고 return 값을 그대로 반환하겠다는 의미입니다.

그래서 "ㅎㅇㅎㅇ"라는 한글이 응답되었습니다.


하지만 결과를 보시면 한글이 깨져서 ????가 브라우저에 출력 될 것입니다.

이를 해결하기 위해 MessageConerveter를 등록해보겠습니다.




spring-servlet.xml

<mvc:annotation-driven>

         <!-- messageConvertert -->

         <mvc:message-converters>

                  <bean class="org.springframework.http.converter.StringHttpMessageConverter">

                           <property name="supportedMediaTypes">

                                   <list>

                                            <value>text/html; charset=UTF-8</value>

                                   </list>

                           </property>

                  </bean>

         </mvc:message-converters>

</mvc:annotation-driven>

위의 설정은 String에 대해서 인코딩 타입이 UTF-8인 text/html 방식으로 응답하겠다는 것입니다.

이렇게 설정을 하고 똑같은 요청을 해보시면 이번에는 "ㅎㅇㅎㅇ"가 제대로 출력 되는 것을 확인할 수 있습니다.


StringHttpMessageConverter bean을 등록했더니 UTF-8로 인코딩 되어 응답이 됩니다.

StringHttpMessageConverter는 응답 메시지인 문자열을 인코딩합니다.



@ResponseBody가 무엇인지 알아보았고, StringHttpMessageConverter를 등록해보았습니다.

이제 본격적으로 JSON을 응답해보겠습니다.





환경 설정

pom.xml

<!-- jackson -->

<dependency>

    <groupId>com.fasterxml.jackson.core</groupId>

    <artifactId>jackson-databind</artifactId>

    <version>2.1.1</version>

</dependency>



spring-servlet.xml

<mvc:annotation-driven>

         <!-- messageConvertert -->

         <mvc:message-converters>

                  <bean class="org.springframework.http.converter.StringHttpMessageConverter">

<property name="supportedMediaTypes">

                                   <list>

                                            <value>text/html; charset=UTF-8</value>

                                   </list>

                           </property>

                  </bean>


                  <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">

<property name="supportedMediaTypes">

<list>

                              <value>application/json; charset=UTF-8</value>

                                  </list>

                           </property>

                  </bean>

         </mvc:message-converters>

</mvc:annotation-driven>

기존에 등록했던 StringHttpMessageConverter bean은 그대로 두고, 새로운 MappingJackson2HttpMessageConverter bean을 추가했습니다.

이번에는 인코딩 타입이 UTF-8이며 JSON으로 응답하도록 설정했습니다.

눈치 챈 분도 계시겠지만 <value>에 작성하는 이 부분은 HTTP 응답 헤더 부분 입니다.



이제 컨트롤러 부분을 수정해보겠습니다.

JoinController

@Controller
public class JoinController {

	@ResponseBody
	@RequestMapping("/join")
	public Map<String, Object> join() {
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("name", "victolee");
		map.put("age", 26);
		
		return map;

	}
}

Map을 사용했다는 점에 주목해주세요.

이제 아까와 같이 http://localhost:8080/messageConverterTest/join 으로 요청을 하시면 JSON으로 응답이 되는 것을 확인할 수 있습니다.


( 저와 같이 JSON을 브라우저에 예쁘게 출력하고 싶으면 크롬 확장 프로그램에서 JSON을 검색하여 JSON viewer를 다운 받으시면 됩니다. )



이와 같이 MessageConverter를 통해 java 객체를 JSON으로 응답하게 되면 Ajax 통신이 가능해집니다.

이제 Ajax를 어떻게 구현할 수 있을 지 간단한 예제로 살펴보겠습니다.





Ajax 예제

index.jsp

<body>

<form id="add-form" action="" method="post">

<input type="text" name="name" id="input-name" placeholder="이름">

<input type="password" name="pwd" id="input-password" placeholder="비밀번호">

<textarea id="tx-content" name="content" placeholder="내용을 입력해 주세요."></textarea>

<input id="input-submit" type="submit" value="보내기" />

        </form>

</body>

Ajax에서는 form으로 데이터를 전달하지 않기 때문에 <form>이 필요 없습니다.

하지만 웹 표준을 지키기 위해 추가했으며, JS 코드에서는 form에서 서버로 데이터가 전송되지 않도록 이벤트를 막을 것입니다.



1) URL Query String으로 데이터 전달 - serialize( ) 메서드

index.jsp

$("#add-form").submit( function(event){

event.preventDefault();


// serialize form <input> 요소들의 name 배열형태로 값이 인코딩되어 URL query string으로 하는 메서드

let queryString = $(this).serialize();


$.ajax({

url: "/ajaxtest/api/guestbook/insert",

type: "POST",

dataType: "json",

data: queryString,

success: function(result){

// ajax 통신 성공 로직 수행

}

         })

});

비동기로 요청을 보낼 것이므로 기본 <form> 태그의 동작을 중지시켜야 합니다.

그러기 위해서는 event.preventDefault() 메서드를 호출하여 기본적으로 동작하는 <form>의 submit 이벤트를 중지 시킵니다.


변수 queryString의 값을 보면 아래와 같이 form의 데이터들이 URL에 queryString으로 전달됩니다.




컨트롤러

@ResponseBody
@RequestMapping("/insert")
public GuestBookVO insert(@ModelAttribute GuestBookVO vo) {
	GuestBookVO guestBookVO = GuestBookService.insert(vo);
	return guestBookVO;
}

지금까지 알아봤던 것처럼 MessageConverter 설정을 등록한 후 컨트롤러에 @ResponseBody 어노테이션을 추가하면 클라이언트로 응답이 될 것입니다.

guestBookVO를 성공적으로 반환하게 되면, ajax의 success 함수의 매개변수 result로 전달이 됩니다.




2) JSON으로 전달 - serializeArray() , stringify( ) 메서드

index.jsp

$("#add-form").submit( function(event){

         event.preventDefault();

         // JSON으로 요청

         var data = {};

         $.each( $(this).serializeArray(), function(index, o){

                  data[o.name] = o.value

         })

         $.ajax({

                  url: "/ajaxtest/api/guestbook/insert",

                  type: "POST",

                  dataType: "json",          // ajax 통신으로 받는 타입

                  contentType: "application/json"// ajax 통신으로 보내는 타입

                  data: JSON.stringify(data),

                  success: function(result){

                           // ajax 통신 성공 로직 수행

                  }

         })

});

이번에는 Ajax 요청을 하기 전에 반복문을 돌면서 객체 data에 프로퍼티, 값을 추가합니다.


그리고 ajax() 메서드 부분에서 contentType 속성에 "application/json"을 작성해주면, JSON으로 요청을 보내겠다는 의미이고,

data 속성에 JSON.stringify() 메서드를 호출하면 JSON이 문자열 형태로 데이터가 전달됩니다.

HTTP는 문자열로 데이터를 주고 받기 때문에 JSON을 문자열로 변환시켜야 합니다.

참고로 stringify() 메서드를 호출한 JSON 객체는 JS 내장 객체입니다.



컨트롤러

@ResponseBody
@RequestMapping("/insert")
public JSONResult insert(@RequestBody GuestBookVO vo) {
	GuestBookVO guestBookvo = GuestBookService.insert2(vo);
	return JSONResult.success(guestBookvo);
}

클라이언트에서 JSON 형태로 데이터를 전달하기 때문에 @RequestBody를 매개변수에 작성합니다.

이 때, @ModelAttribute를 함께 작성하면 충돌이 발생하므로 제거했으며, @ModelAttribute는 생략해도 객체를 파라미터로 받을 수 있습니다.





ViewResolver?? MessageConverter??

지금까지 컨트롤러에서 응답을 할 때 String으로 반환하면 뷰 페이지를 응답해주는 것으로 알고 있었습니다.

그런데 @ResponseBody가 있으면 문자열을 그대로 반환해주는데, 반환되는 문자열에 대해 ViewResolver가 실행될지, MessageConverter가 실행될지 어떻게 알 수 있을까요?


DispatchServlet은 handlerMethod()를 호출하여 응답으로 String , ModelAndView , Object 중 하나를 반환합니다.

이후 실행되는 것이 ViewResolver , MessageConverter 인데, DispatchServlet이 반환 되는 타입을 보고 어떤 처리를 할지 결정합니다.


만약 String이 반환되면 뷰 이름을 찾아서 JSP를 렌더링 하고, 뷰가 없으면 404를 반환합니다.

그런데 메서드 위에 @ResponseBody 어노테이션이 있다면 뷰를 찾지 않고, String을 그대로 반환합니다.


Dispatcher Servlet이 ModelAndView를 반환하면 ViewResolver가 실행됩니다.

그런데 @ResponseBody가 있으면 Object는 MessageConverter가 실행되어 반환됩니다.




이상으로 클라이언트에게 다양한 형식으로 반환할 수 있도록 도와주는 MessageConverter에 대해 알아보았고,

간단하게 Ajax 통신과 JSON으로 전달하는 방법에 대해 알아보았습니다.