본문 바로가기

프로그래밍/Spring

Spring Framework의 ContentNegotiatingViewResolver에 대하여... (3)

ContentNegotiatingViewResolver에 대하여 XML로 출력하는 것에 대해서는 앞에서 살펴보았다. 그럼 이제 JSON과 Text 출력에 대해 알아보도록 한다

 

2) JSON(MappingJacksonJsonView)

 

XML로의 출력은 사람이 알아보기 쉬운 장점이 있으나 실제 사용하는 데이터에 비해 부가적인 정보들이 지나치게 많이 내려가는 문제가 있다(태그명 등..) 이런 점을 해소하기 위해 나온것이 JSON 포맷이다. 최대한 군더더기가 없는 실제 사용되는 데이터만 내려가므로 데이터 전송량이 XML에 비하면 상대적으로 작지만 한편으로는 사람 눈으로 알아보기엔 직관적이지 않은 구조가 단점이다. 그러나 컴퓨터가 잘 알아보기 때문에 이 부분은 패스.. 이 문서에서는 JSON 포맷에 대한 구체적인 언급은 않겠다(JSON 포맷에 대해 좀더 알고 싶은 분은 갠적으로 구글링을 하세요)

 

Spring에서도 JSON 포맷을 지원하는 View 가 있는데 그것은 org.springframework.web.servlet.view.json.MappingJacksonJsonView 클래스이다. 이 View를 사용하면 Spring의 컨트롤러가 객체를 리턴할 경우 객체의 내용을 분석하여 이를 JSON 포맷으로 변환하여 보내주게 된다. 그러나 이것을 바로 이용할 수는 없었다. 적어도 내가 진행하고 있던 프로젝트에서는 말이다.

 

MappingJacksonJsonView를 이용해서 객체를 JSON 포맷으로 리턴하면 다음과 같은 문제가 발생한다. 예를 들어 앞에서 다루었던 Result와 InfoVO 객체를 JSON으로 변환하면 다음의 결과가 나타난다

 

{
	"result":
	{
            "recordset":[
    	        {
               	    "info_code":"01",
                    "info_name":"마이인포 01"
                },
                {
              	    "info_code":"02",
                    "info_name":"마이인포 02"
                },
                {	
             	    "info_code":"03",
              	    "info_name":"마이인포 03"
                },
                {
              	    "info_code":"04",
                    "info_name":"마이인포 04"
                }
            ],
            "message":"성공",
            "flag":"0000"
	}
}

 

어딘가 이상한점을 발견하지 못했는가? 사실 우리가 표현하는 JSON 문자열에서 result 항목은 필요가 없다. 무슨 말이냐면 우리가 원하는건 최상위에 result란 멤버가 있고 그 밑에 recordset 멤버와 message 멤버와 flag 멤버를 보는 것이 아니라 최상위에 recordset, message, flag 멤버가 있어야 한다는 것이다(물론 이런 상황을 프로젝트가 용인하면 상관없지만 그렇게 용인할 프로젝트가 그리 있지는 않으리라 생각한다) 바로 이런 점으로 인해 MappingJacksonJsonView 클래스를 바로 사용할수가 없었다. 이것을 우리가 원하는 결과가 나타나게끔 할려면 MappingJacksonJsonView 클래스를 상속받아 다음과 같이 새로운 View 클래스를 만들어서 사용하면 된다

 

public class MyMappingJacksonJsonView extends MappingJacksonJsonView {

	@SuppressWarnings("unchecked")
	@Override
	protected Object filterModel(Map<String, Object> model) {
		// TODO Auto-generated method stub
		Object result = super.filterModel(model);
		if (!(result instanceof Map)) {
		    return result;
		}
		Map map = (Map) result;
		if (map.size() == 1) {
			return map.values().toArray()[0];
		}
		return map;
	}

}

 

그리고 이렇게 만든 Custom MappingJacksonJsonView 클래스를 ContentViewNegotiatingViewResolver에서 다음과 같이 사용하면 된다

 

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
	<property name="order" value="1" />
	<property name="favorPathExtension" value="false" />
	<property name="mediaTypes">
		<map>
			<entry key="xml" value="application/xml" />
			<entry key="json" value="application/json" />
			<entry key="text" value="text/plain"/>
		</map>
	</property>
	<property name="ignoreAcceptHeader" value="false" />
	<property name="defaultViews">
		<list>
			<bean class="myprj.view.MyMappingJacksonJsonView" />
		</list>
	</property>
</bean>

 

이렇게 하면 기존의 JSON 결과가 다음과 같이 바뀌어 나타난다

 

{
	"recordset":[
    	{
            "info_code":"01",
            "info_name":"마이인포 01"
        },
        {
            "info_code":"02",
            "info_name":"마이인포 02"
        },
        {
            "info_code":"03",
            "info_name":"마이인포 03"
        },
        {
            "info_code":"04",
            "info_name":"마이인포 04"
        }
    ],
    "message":"성공",
    "flag":"0000"
}

3) Text(TextPlainView)

 

이 프로젝트에서 요구했던 Text 포맷으로의 전달은 클래스의 멤버변수에 저장되어 있는 정보를 구분자를 이용하여 연결한 하나의 문자열로 내려보내주는 것이었다. 근데 Spring에서는 이런 기능을 하는 View 클래스가 존재하질 않는다. 그래서 이 경우는 Custom View 클래스로 자체제작을 해서 해결하였다. MappingJacksonJsonView 클래스의 소스를 참조하며 만들었는데 소스는 다음과 같다.

 

public class TextPlainView extends AbstractView {

	public static final String DEFAULT_CONTENT_TYPE = "text/plain";
	public static final String encoding = "UTF-8";

	private Set<String> renderedAttributes;
	
	private boolean disableCaching = true;
	
	private final Log logger = LogFactory.getLog(this.getClass());
	
	public TextPlainView(){
		setContentType(DEFAULT_CONTENT_TYPE);
	}
	
	
	public Set<String> getRenderedAttributes() {
		return renderedAttributes;
	}


	public void setRenderedAttributes(Set<String> renderedAttributes) {
		this.renderedAttributes = renderedAttributes;
	}


	@Override
	protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
		response.setContentType(getContentType());
		response.setCharacterEncoding(encoding);
		
		if (disableCaching) {
			response.addHeader("Pragma", "no-cache");
			response.addHeader("Cache-Control", "no-cache, no-store, max-age=0");
			response.addDateHeader("Expires", 1L);
		}
	}
	
	
	@Override
	protected void renderMergedOutputModel(Map<String, Object> model,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		HashMap<String, Object> map = (HashMap<String, Object>)(filterModel(model));
		String result = "";
		Iterator<String> iterator = map.keySet().iterator();
		while(iterator.hasNext()){
			String key = (String)iterator.next();
			result = map.get(key).toString();
		}
		
		Writer out = response.getWriter();
		out.append(result);
	}
	
	protected Object filterModel(Map<String, Object> model) {
		Map<String, Object> result = new HashMap<String, Object>(model.size());
		Set<String> renderedAttributes =
				!CollectionUtils.isEmpty(this.renderedAttributes) ? this.renderedAttributes : model.keySet();
		for (Map.Entry<String, Object> entry : model.entrySet()) {
			if (!(entry.getValue() instanceof BindingResult) && renderedAttributes.contains(entry.getKey())) {
				result.put(entry.getKey(), entry.getValue());
			}
		}
		return result;
	}

}

 

아시는 분도 있겠지만 Spring MVC에서 출력으로 사용되는 View 클래스는 org.springframework.web.servlet.view.AbstractView를 상속받아서 구현하게 된다. 이 Text 포맷 출력 View도 예외는 아니다. 여기서 눈여겨 봐둬야 할 함수는 renderMergedOutputModel 함수이다. 이 함수는 Controller 에서 전달된 Model 객체를 받아다가 Model 객체 안에 있는 객체들을 꺼내서 HttpServletResponse 객체에 출력하게 되는데 우리는 Text 포맷으로 출력해야 하기 때문에 모든 객체가 가지고 있는  toString() 메소드를 이용하여 출력하게 된다. 앞에서 우리가 만든 Result 클래스와 InfoVO 클래스의 소스를 보면 toString() 메소드가 override 되어 있다. 예를 들어 앞에서 보여줬던 InfoVO 클래스의 toString() 메소드 내용만을 따로 보면 다음과 같다.

 

@Override
public String toString() {
	// TODO Auto-generated method stub
	StringBuffer sb = new StringBuffer();
	sb.append(getInfo_code());
	sb.append("__");
	sb.append(getInfo_name());
	return sb.toString();
}

 

이 메소드의 소스를 보면 InfoVO 클래스의 멤버변수인 info_code와 info_name의 gettter 함수인 getInfo_code() 함수와 getInfo_name() 함수를 이용하여 각 멤버변수의 값을 꺼내온뒤 구분자인 __로 두 멤버변수의 값을 연결하여 이를 String으로 return 하는 것이다. 이렇게 Result 클래스와 InfoVO 클래스의 toString() 메소드를 이용하여 전달해야 할 전체 문자열을 만든뒤에 이를 HttpServletResponse 객체의 Writer 객체에 전달하여 HttpSetvletResponse의 Output stream에  문자열이 쓰여지게 된다. 이렇게 출력된 문자열의 결과는 다음과 같다

 

0000__성공_^01__마이인포 01#!02__마이인포 02#!03__마이인포 03#!04__마이인포 04

 

이렇게 전달되어진 문자열을 구분자(__, _^, #!)로 분리하여 데이터를 뽑아낸다. 이렇게 만들어낸 TextPlainView를 Spring에서는 다음과 같이 사용한다

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
	<property name="order" value="1" />
	<property name="favorPathExtension" value="false" />
	<property name="mediaTypes">
		<map>
			<entry key="xml" value="application/xml" />
			<entry key="json" value="application/json" />
			<entry key="text" value="text/plain"/>
		</map>
	</property>
	<property name="ignoreAcceptHeader" value="false" />
	<property name="defaultViews">
		<list>
			<bean class="myprj.view.TextPlainView">
				<property name="contentType" value="text/plain" />
			</bean>
		</list>
	</property>
</bean>

 

이해를 쉽게 하기 위해 설정을 따로따로 분리하여 설정하는 식으로 설명하였으나 초반에 말했다시피 XML, JSON, Plain Text 로 결과를 받을수 있어야 하기 때문에 앞에서 말한 설정들이 모두 되어야 한다. 이렇게 하는 설정이 다음과 같다

 

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
	<property name="order" value="1" />
	<property name="favorPathExtension" value="false" />
	<property name="mediaTypes">
		<map>
			<entry key="xml" value="application/xml" />
			<entry key="json" value="application/json" />
			<entry key="text" value="text/plain"/>
		</map>
	</property>
	<property name="ignoreAcceptHeader" value="false" />
	<property name="defaultViews">
		<list>
			<bean class="org.springframework.web.servlet.view.xml.MarshallingView">
				<constructor-arg>
					<bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
						<property name="classesToBeBound">
							<list>
								<value>myprj.vo.Result</value>
								<value>myprj.vo.Info</value>
							</list>
						</property>
						<property name="marshallerProperties">
							<map>
								<entry>
									<key>
										<util:constant static-field="javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT" />
									</key>
									<value type="java.lang.Boolean">false</value>
								</entry>
							</map>
						</property>
					</bean>
				</constructor-arg>
			</bean>
			<bean class="myprj.view.MyMappingJacksonJsonView" />
			<bean class="myprj.view.TextPlainView">
				<property name="contentType" value="text/plain" />
			</bean>
		</list>
	</property>
</bean>