본문 바로가기

spring

Spring을 이용한 RESTful 서비스 3


2012/07/11 - [spring] - Spring을 이용한 RESTful 서비스 1

2012/07/11 - [spring] - Spring을 이용한 RESTful 서비스 2

이미 만들어진 RESTful 서비스를 jQuery를 이용해서 사용해보겠습니다.

jQuery의 $.ajax() 함수를 이용해서 호출을 한다면 다음과 같이 목록을 가져오는 서비스(@RequestMapping(value = "/memos", method = RequestMethod.GET))를 사용할 수 있습니다.

jquery.jsp

 $.ajax({

        type: "get",

        accepts: "application/json",

        async: true,

        url: "http://localhost:8080/restservice/memos",

        contentType: "application/json",

        dataType: "json",

        beforeSend: function(xhr) {

               xhr.setRequestHeader("Accept", "application/json");

        },

        error: function(xhr, status, error){

               alert("xhr:"+xhr+", status:"+status+", error:"+error);

        },

        success: function(data, status, xhr) {

               alert(data.result);

        }

});

하지만 대부분 이런형태의 서비스는 도메인이 틀린 경우가 많습니다. javascript는 같은 도메인안에서만 동작하는 Same-Origin Policy을 가지고 있습니다.

그래서 proxy 방식으로 구현하던지 아니면 JSONP를 사용하게 됩니다. 여기서는 jQuery가 지원해주는 JSONP를 이용해서 서비스를 사용해보겠습니다. 

서비스의 구성은 MappingJacksonHttpMessageConverter를 이용해서 JSON형식으로 자동변환되어 응답하게 구현되어 있는데 JSONP를 사용할려면 콜백함수 형태로 응답이 되어야 합니다. 가장 간단한 방법으로는 @ResponseBody를 이용한 자동 메세지 컨버팅을 포기하고 직접 view를 하나 정의(http://vivin.net/2011/07/01/implementing-jsonp-in-spring-mvc-3-0-x/)하여 구현하면 됩니다.

하지만 @ResponseBody 방식을 사용하고 싶어서 MappingJsonpHttpMessageConverter를 상속받아 다음 메세지 컨버터를 구현하였습니다. writeInternal을 overriding하고, 콜백함수를 만들기 위한 prefixCallback, suffixCallback 속성을 추가하였습니다.

MappingJsonpHttpMessageConverter.java

package com.tistory.aircook.common.converter;

 

import java.io.IOException;

 

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.codehaus.jackson.JsonEncoding;

import org.codehaus.jackson.JsonGenerator;

import org.codehaus.jackson.JsonProcessingException;

import org.codehaus.jackson.map.util.JSONWrappedObject;

import org.springframework.http.HttpOutputMessage;

import org.springframework.http.converter.HttpMessageNotWritableException;

import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;

 

/**

 * jsonp용 메세지컨버터

 * @author francis Lee

 * @since 2012. 07. 12.

 */

public class MappingJsonpHttpMessageConverter extends MappingJacksonHttpMessageConverter {

        private final Log logger = LogFactory.getLog(getClass());

        private boolean prefixJson = false;

        // jsonp, callback 함수 접두어

        private String prefixCallback = "callback(";

        // jsonp, callbak 함수 접미어

        private String suffixCallback = ");";

        public void setPrefixJson(boolean prefixJson) {

               this.prefixJson = prefixJson;

        }

        public void setPrefixCallback(String prefixCallback) {

               this.prefixCallback = prefixCallback;

        }

        public void setSuffixCallback(String suffixCallback) {

               this.suffixCallback = suffixCallback;

        }

 

        @Override

        protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException,

                       HttpMessageNotWritableException {

               JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());

               JsonGenerator jsonGenerator = super.getObjectMapper().getJsonFactory()

                              .createJsonGenerator(outputMessage.getBody(), encoding);

               try {

                       if (this.prefixJson) {

                              jsonGenerator.writeRaw("{} && ");

                       }

                       // 접두어, 접미어 추가

                       JSONWrappedObject jsonWrappedObject = new JSONWrappedObject(prefixCallback, suffixCallback, object);

                       super.getObjectMapper().writeValue(jsonGenerator, jsonWrappedObject);

               }

               catch (JsonProcessingException ex) {

                      throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);

               }

        }

}

이제 MappingJsonpHttpMessageConverter 를 사용하기 위해서 Spring 설정파일을 아래와 같이 수정했습니다.

spring-servlet.xml

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"

        p:order="0" />

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"

        p:messageConverters-ref="messageConverters" />

<util:list id="messageConverters">

        <bean class="com.tistory.aircook.common.converter.MappingJsonpHttpMessageConverter"

               p:supportedMediaTypes="application/javascript" p:prefixCallback="callback("

               p:suffixCallback=");" />

        <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"

               p:supportedMediaTypes="application/json" />

</util:list>

위설정을 Spring 3.1에서는 다음과 같이 사용할 수도 있습니다. 물른 xml schema를 3.1(xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd")로 변경해야 사용가능합니다.

spring-servlet.xml

<mvc:annotation-driven>

        <mvc:message-converters>

               <bean class="com.tistory.aircook.common.converter.MappingJsonpHttpMessageConverter"

                       p:supportedMediaTypes="application/javascript" p:prefixCallback="callback("

                       p:suffixCallback=");" />

               <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"

                       p:supportedMediaTypes="application/json" />

        </mvc:message-converters>

</mvc:annotation-driven>

아래는 완성된 jsp입니다. jQuery에서 JSONP를 사용할 경우 콜백함수명은 자동으로 부여되나, 메세지컨버터에서 그 이름값을 가져올 방법이 확실하게 없어서 jsonpCallback을 사용해 함수명을 고정하였습니다.

jquery.jsp

<%@ page language="java" pageEncoding="UTF-8" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

 

<%

/**

 * RESTful jQuery TEST

 * @author francis Lee

 * @since 2012. 07. 11.

 */

%>

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="utf-8">

<title>INDEX</title>

<style type="text/css">

body {

        font-family: "Malgun Gothic","돋움","Trebuchet MS","Helvetica","Arial","Verdana","sans-serif";

        font-size: 14px;

}

input, textarea{

        font-family: "Malgun Gothic","돋움","Trebuchet MS","Helvetica","Arial","Verdana","sans-serif";

        font-size: 12px;

}

table {

        border-collapse:collapse;

}

table, td, th {

        border:1px solid black;

}

#output {

        width:800px;

        height:50px;

        background-color:#CCCCCC;

}

</style>

<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.7.2.min.js" charset="utf-8"></script>

<script type="text/javascript" src="<c:url value='/js/jquery.ba-serializeobject.min.js'/>" charset="utf-8"></script>

<script type="text/javascript" src="<c:url value='/js/json2.js'/>" charset="utf-8"></script>

<script type="text/javascript" charset="utf-8">

$(document).ready(function(){

       

        //추가

        $("#addHandler").click(function(){

               $.ajax({

                       type: "post",

                       accepts: "application/json",

                       url: "http://localhost:8080/restservice/memos",

                       data: JSON.stringify($("form").serializeObject()),

                       contentType: "application/json",

                       dataType: "json",

                       beforeSend: function(xhr) {

                              xhr.setRequestHeader("Accept", "application/json");

                       },

                       error: function(xhr, status, error){

                              alert("xhr:"+xhr+", status:"+status+", error:"+error);

                       },

                       success: function(data, status, xhr) {

                              alert(data.result);

                              $("#output").html(JSON.stringify(data));

                       },

                       complete: function() {

                       }

               });                          

        });    

       

        //목록(jsonp)

        $("#listHandler1").click(function(){

               $.ajax({

                       type: "get",

                       accepts: "application/javascript",

                       url: "http://localhost:8080/restservice/memos",

                       contentType: "application/javascript",

                       dataType: "jsonp",

                       jsonpCallback : "callback",

                       jsonp : "callback",

                       crossDomain: true,

                       beforeSend: function(xhr) {

                              xhr.setRequestHeader("Accept", "application/javascript");

                       }, 

                       error: function(xhr, status, error){

                              alert("xhr:"+xhr+", status:"+status+", error:"+error);

                       },

                       success: function(data, status, xhr) {

                              alert(data.result);

                              $("#output").html(JSON.stringify(data));

                       },

                       complete: function() {

                       }

               });                          

        });    

       

        //목록(json)

        $("#listHandler2").click(function(){

               $.ajax({

                       type: "get",

                       accepts: "application/json",

                       async: true,

                       url: "http://localhost:8080/restservice/memos",

                       contentType: "application/json",

                       dataType: "json",

                       beforeSend: function(xhr) {

                              xhr.setRequestHeader("Accept", "application/json");

                       },

                       error: function(xhr, status, error){

                              alert("xhr:"+xhr+", status:"+status+", error:"+error);

                       },

                       success: function(data, status, xhr) {

                              alert(data.result);

                              $("#output").html(JSON.stringify(data));

                       },

                       complete: function() {

                       }

               });                                  

        });    

       

});

</script>

</head>

<body>

<form>

<h2>ADD</h2>

<div>

memo : <input type="text" name="memo" />

<input type="button" value="DO ADD" id="addHandler" />

</div>

<hr/>

<h2>LIST</h2>

<div>

<input type="button" value="DO LIST1 (JSONP)" id="listHandler1" />

<input type="button" value="DO LIST2 (JSON)" id="listHandler2" />

<br/>

<textarea id="output"></textarea>

</div>

</form>

</body>

</html>

위 jsp를 같은 도메인에 두고 list1, list2 버튼을 클릭하면 둘다 동작을 하고, 다른 도메인에 두고 실행을 하면 JSONP인 list1만 동작하는걸 확인할 수 있습니다.

RESTClinet 플러그인을 통해 header 속성중 Accept, Content-Type을 application/javascript으로 요청하면 다음과 같이 응답에 "callback("이라는 접두어와 ");" 이라는 접미어가 붙어 있는것을 확인할 수 있습니다.

JSONP 요청

JSON 요청

문제점이 하나 있는데!!

jQuery $.ajax()의 settings값들중 type에 대한 설명은 아래와 같습니다. 모든브라우저가 PUT, DELETE같은 method를 지원안해준다고 나옵니다. RESTful형식에서는 method로 구분되어 요청을 받게 처리되는데 처리가 안되는 브라우저도 있다는 이야기 입니다.

typeString
Default: 'GET'

The type of request to make ("POST" or "GET"), default is "GET". Note: Other HTTP request methods, such as PUT and DELETE, can also be used here, but they are not supported by all browsers.

이 문제를 해결하기 위해 서비스쪽에 서블릿 필터(HiddenHttpMethodFilter)를 하나 걸어서 해결하는걸로 알고 있습니다. 소스를 살펴보니 요청이 "POST"이고 "_method" 라는 파라미터 값이 있으면 이 값을 이용해서 요청메소드를 변경해주는 기능을 합니다.

이걸 사용하면 해결이 되는데 진행해온 방식이 JSON형식의 문자열을 던지면 메세지컨버터가 자바 오브젝트로 자동 변환해주는 방식인데 요청본문(Reqeust Body)에 이 값이 같이 들어가 있으면 메세지컨버터가 정상동작을 안할것 같습니다.

시간될때 HiddenHttpMethodFilter를 가지고 JSON형식의 문자열에서 특정값을 뽑아 요청을 변경해줄수 있는 필터를 작성해봐야겠습니다. 별로 어렵지는 않겠죠! 상황이 이렇게 되니 그냥 proxy 형태로 구현하는게 더 쉬워보입니다. ㅡ.ㅡ;

또다른 문제점

흠 생각지도 않았는데.. JSONP일때는 무조건 GET으로 요청이 갑니다. 아! 이건 어떻게 해결해야 될지 딱히 방법이..?