IT_Programming/Dev Libs & Framework

[펌] Spring - XML, JSON 출력관련: ContentNegotiatingViewResolver에 대하여...

JJun ™ 2013. 2. 21. 14:20

 


 

 출처: http://zgundam.tistory.com/16

 

 아래 글과 비슷한 것을 Spring을 사용해서 만들게 되어 자료조사 하다가 발견한 글입니다.

 


 

 


 

3월달에서 시작한 프로젝트가 6월에 마무리가 되었다. 업체명과 어떤일을 했는지 상세하게 말할수 없지만 이번에 했던 일은 웹사이트 구축이 아니었다.
하드웨어 단말에서 웹URL을 호출하여 리턴되는 XML이나 
JSON 또는 Text 문자열을 파싱하여 그에 따른 결과값으로 작업을 하는 일이다.

어찌보면 Open API와 비스무리한 일을 했다.

 

이번에 이 프로젝트의 아키텍처 역할을 하면서 Spring 기반으로 작업을 진행했는데 이 때 사용했던 기술을 언급하고자 한다.

업무의 기술적인 큰 요구는 2가지였다

 

1) 단말이 원하는 데이터를 XML, Json, 또는 Plain Text로 받아야 한다. 단 이때 호출하는 URL이 리턴받는 데이터 타입에 따라 다른 형태면 안된다.
   (사실 이 조건은 필수적인 조건은 아니었으나 업무를 하면서 그런 뉘앙스를 많이 받았다) 


2) 단말이 자주 억세스 하는 데이터는 캐쉬로 보관해서 전달해야 한다.

   이 부분은 ContentNegotiatingViewResolver 다음에 언급할 EhCache 를 다룰때 구체적으로 언급하겠다

 

 

1번의 사항을 보고 생각이 났던것은 ContentNegotiatingViewResolver였다.
나도 스프링 경력이 긴 편은 
결코 아니지만 검색을 해보면 이것을 이용해서 프로젝트를 수행한 내용은 별로 보이질 않았다.

하지만 암튼 그당시로써는 저게 생각이 나는지라 ContentNegotiatingViewResolver로 이 부분을 공략해보기로 했다


모든게 그렇지만 한번에 해결되지 않아 쉽지만은 않았다. 스프링 프로젝트 템플릿을 만들어 팀원들에게 배포한 뒤
ContentNegotiatingViewResolver를 이용한 예제를 만들어갔다. 잘 안풀렸던것은 2가지였다

(위에 것도 그렇고 지금도 그렇고 모든게 2가지?)

 

1) 확장자를 붙여야 원하는 포맷으로 리턴하는 문제
   (예를 들어 xml로 리턴받기 위해선 확장자를 xml로 붙여야 했고 json으로 리턴받기 위해선 json으로 확장자를 붙여야했다)


2) 어찌보면 순서가 바뀌긴 했는데 안드로이드 단말과 연동해서 서버단을 개발해본 같은 프로젝트 팀원이 소개해준 테스트용 Firefox 플러그인(HttpRequester)를
   이용한 테스트 결과 확인법이 제대로 되질 않음

 

2번의 문제는 HttpRequester의 문제가 아니라 HttpRequester의 사용법 숙지를 제대로 하지 않아 발생했던 문제였다(이 문제땜에 하루를 낭비..ㅠㅠ..)
그리고 1번의 문제는 ContentNegotiatingViewResolver의 ignoreAcceptHeader 프로퍼티값을 false로 주어 이 문제를 해결했다.
이제 1번 문제에 대한 설명을 하도록 하겠다

 

구글링을 통해 얻었던 ContentNegotiatingViewResolver의 사용 예제들에선 ignoreAcceptHeader의 프로퍼티 값을 true로 준 것들뿐이었다.
ContentNegotiatingViewResolver가 리턴 포맷을 결정하는 근거로는 4가지 중 하나를 사용한다

 

1) URL에 붙는 확장자

2) URL의 특정 파라미터에 설정되는 값

3) Request Header중 Accept 항목에 설정되어 있는 값

4) 1),2),3)번을 모두 사용하지 않을 경우 defaultContentType 프로퍼티에서 정해진 값

 

1번에 대해서는 위에 잠깐 언급했지만 확장자를 xml로 설정해서 호출할 경우 리턴 포맷이 xml이 되며 json으로 호출할 경우 json으로 리턴이 된다.
하지만 이 프로젝트에서는 그렇게 할 수는 없었다. 
이 프로젝트의 요구 사항은 위에서도 잠깐 언급했다시피 호출하는 URL이 다르면 안되는 것이었기 때문이다.
그래서 2번도 사용할 수 없었고 3번을 써야만 했다. 이 3번을 쓸려면 구글링을 통해서 얻었던 ignoreAcceptHeader의 true을 false로 바꿔줘야 한다는 것이다

 

Spring에서 제공하는 ContentNegotiatingViewResolver의 소스를 보면 다음의 변수 선언이 있다

   private static final String ACCEPT_HEADER = "Accept";

 

 

이렇게 선언되어 있어서 ignoreAcceptHeader 프로퍼티의 값을 false로 줄경우 ContentNegotiatingViewResolver 에서 HttpRequest Header의 Accept 항목 값을 보고
리턴하는 포맷을 결정하게 된다. 
물론 이 부분을 Accept가 아닌 다른 이름의 항목으로 바꿀수도 있다. 하지만 ContentNegotiatingViewResolver를 그대로 사용하면서
바꿀수는 없다. (static final이므로 외부에서 
변경을 할 수 없기 때문이다) 이 부분은 ContentNegotiatingViewResolver를 상속받아서 작업하면 된다.

상속받은 클래스에서 ACCEPT_HEADER의 변수값을 재설정을 하면 된다

 

나는 프로젝트를 하면서 이 클래스를 상속을 해서 사용하진 않았다.
어찌보면 참으로 둔한짓을 했는데 그것은 ContentNegotiatingViewResolver 클래스의 소스를 복사해서 내가 만든 새로운 클래스에 붙여넣은 뒤에 관련된 작업을
했기 때문이다. 상속을 했어야 하는건데..ㅠㅠ..

 

그러면 이렇게 정해진 ACCEPT_HEADER  변수를 어디서 사용하는가?

ContentNegotiatingViewResolver 클래스에는 다음의 함수가 있다

 

 
protected List<mediatype> getMediaTypes(HttpServletRequest request)

 

 

이 함수가 하는 일은 리턴 포맷을 결정할 MediaType을 가져오는 것인데 이 함수의 소스를 보면 MediaType을 가져올때 그 판단하는 기준이 코딩되어 있다

ContentNegotiatingViewResolver 클래스엔 favorPathExtension란 boolean형 프로퍼티가 있다.

(프로퍼티이므로 setter가 존재한다. 고로 Spring의 환경 파일에서 이 변수에 대한 값을 바꿀수 있다) 


이 변수가 true이면 ContentNegotiatingViewResolver 클래스는 리턴 포맷의 판단 기준을 URL로 보고 URL에서 MediaType을 구한뒤 그것을 리턴하게 된다.
그리고 이 멤버변수는 디폴트로 true로 되어 있다. 
그래서 지금 할려고 하는 HttpRequest Header에서 해당 리턴타입을 결정짓게 할려면 이 프로퍼티를 false 해야 한다

 

그리고 이 함수에서는 또 하나의 판단 기준을 사용하는데 그것은 URL 호출시 특정 파라미터로 리턴포맷을 전달하는 것도 허용하는 것이다.
즉 a.do?format=xml 요렇게 던져주면 Spring에서는 format 파라미터 값을 
읽어서 그걸로 결정하는 방법이다.
이러한 방법은
favorParameter 프로퍼티와 parameterName이란 프로퍼티를 이용해서 할수가 있다.
(favorParameter는 디폴트로 false로 설정되어 있고 parameterName은 
디폴트로 format 으로 설정되어 있다)

 

그래서 이런 모든 판단 기준을 거친뒤에 세번째의 판단기준을 거치는 것이 바로 ignoreAcceptHeader 프로퍼티 값을 보고 판단하는 것으로 오게 된다.
그리고 이걸 거친뒤에 defaultContentType 프로퍼티를 
보고 판단하는 것이다. getMediaTypes 함수에서 판단하는 순서는 다음과 같이 정리하게 된다

 

favorPathExtension 프로퍼티 값을 보고 URL의 확장자에서 리턴 포맷을 결정할지를 판단 -> favorParameter와 parameterName 프로퍼티의 값을 보고
URL의 파라미터 값으로 리턴포맷을 
결정할지를 판단 -> ignoreAcceptHeader 프로퍼티의 값을 보고 HttpRequest Header의 Accept 항목에서 리턴포맷을
결정할지를 판단-> defaultContentType 프로퍼티에 정해져 있는 값으로 리턴포맷을 
결정할지를 판단

 

이렇게 4가지의 기준으로 ContentNegotiatingViewResolver는 리턴포맷을 결정하게 된다.
그리고 여기 판단기준에 모두 만족하지 않으면 ContentViewResolver는 자기가 View를 만들것이 아니라 판단하고 다음 순위의 ViewResolver를 사용하게 된다

 

이와 같은 내용을 종합적으로 정리해서 설정한 내용이 다음과 같다

 <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" />
</bean>

 

 

Request Header의 Accept 항목으로 mediaTypes 프로퍼티에 정해져 있는 application/xml, application/json, text/plain이 들어왔을 경우에
XML, Json, 텍스트 포맷으로 전달하겠다는 설정이다. 
이 설정은 완전한 설정이 아니다. 왜냐면 application/xml, application/json, text/plain 이 들어왔을 경우에

정해진 포맷으로 변해져 사용되어질 View에 대한 정의가 빠져 있다. 다음엔 이 View에 대한 내용을 다루도록 하겠다.

 

 

저번엔 내가 했던 프로젝트의 요구사항과 그로 인한 ContentNegotiatingViewResolver 의 리턴 타입 설정에 대해 얘기를 했었다.
이젠 ContentNegotiatingViewResolver에서 리턴 포맷이 결정된 뒤 이 포맷으로 결과를 만들어주게 될 View에 대해 언급해보도록 하겠다.

 

저번에 프로젝트에서 요구했던 결과 포맷은 XML, JSON, 그리고 일반 텍스트라고 말했었다.

그럼 이 부분에 대해 하나씩 언급해보도록 하겠다.

 

Spring의 컨트롤러에서 함수가 다음과 같이 객체를 리턴하게 되면 ContentViewResolver는 리턴포맷을 결정하여 그에 따른 작업을 진행하게 된다..

 public Result<InfoVO> getInfo(HttpServletRequest request, HttpServletResponse response)

 

 

Result 클래스는 작업 결과 코드와 작업 결과 메시지, 그리고 결과셋을 갖게 되는 클래스로서

다음과 같은 형태로 되어 있다

 

@XmlRootElement
(name="result")
@XmlType(name="Result", namespace="myprj.vo.Result", propOrder={"flag", "message", "recordset"})
public class Result<T> {
     
    String flag = "";         // 작업 결과 코드
    String message = "";      // 작업 결과 메시지
     
    List<T> recordset;   // 결과셋
     
    public Result(){
         
    }
 
    public String getFlag() {
        return flag;
    }
 
    @XmlElement(name="flag")
    public void setFlag(String flag) {
        this.flag = flag;
    }
 
    public String getMessage() {
        return message;
    }
 
    @XmlElement(name="message")
    public void setMessage(String message) {
        this.message = message;
    }
 
    @XmlElementWrapper(name="recordset")
    @XmlElementRefs({
        @XmlElementRef(name="record", type=InfoVO.class)
    })
    public List<T> getRecordset() {
        return recordset;
    }
 
    public void setRecordset(List<T> recordset) {
        this.recordset = recordset;
    }
 
    @Override
    public String toString() {
        // TODO Auto-generated method stub
        StringBuffer result = new StringBuffer();
        result.append(flag);
        result.append("__");
        result.append(message);
         
        if(recordset != null){
            if(recordset.size() != 0){
                StringBuffer record = new StringBuffer();
                record.append("_^");
                boolean start = true;
                for(T item : recordset){
                    if(start){
                        start = false;
                    }else{
                        record.append("#!");
                    }
                    record.append(item);
                }
                result.append(record.toString());
            }
        }
         
        return result.toString();
    }
 

 

 

그리고 이 Result 클래스의 결과셋으로 들어가는 InfoVO 클래스는 다음과 같다

 

@XmlRootElement(name = "result")

@XmlType(name="Info", namespace="myprj.vo.Info", propOrder={"info_code", "info_name"})
public class InfoVO

{

    String info_code;

    String info_name;
     
    public String getInfo_code() {
        return info_code;
    }
     
    @XmlElement(name="info_code")
    public void setInfo_code(String info_code) {
        this.info_code = info_code;
    }
     
    public String getInfo_name() {
        return info_name;
    }
     
    @XmlElement(name="info_name")
    public void setInfo_name(String info_name) {
        this.info_name = info_name;
    }
 
    @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();
    }

}

 

 

 

순수하게 데이터만 담는 VO 역할 클래스이다.
이제 이 2개의 클래스를 이용해서 View에 대한 얘기들을 하겠다

 

 

1) XML (MarshallingView)


자바와 객체의 변환에 있어 흔히 사용되는 말이 바로 마샬링(Marshalling)과 언마샬링(Unmarshalling)이다. 마샬링은 자바 객체를 XML로 변환하는 작업을 말하고
언마샬링은 그와는 반대로 XML을 자바 객체로 변환하는 작업을 일겉는다. 여기서 설명하고자 하는 것은 Spring Controller에서 자바 객체를 리턴했을 때

이를 XML로 변환해서 사용하게 되는 즉 마샬링에 대한 얘기이다.

ContentNegotiatingViewResolver에서 마샬링 작업에 사용되는 View 클래스는 org.springframework.web.servlet.view.xml.MarshallingView 클래스이다.

하지만 이것만으로는 자바 객체를 XML로 변환할 수 없다.

 

자바 객체를 XML로 변환하는 기능을 제공하는 라이브러리는 많이 존재하는데 그 중 어떤것을 사용할지를 지정해야 한다.

 

Spring에서도 이를 위해 다양한 XML 변환 기능을 수행하는 클래스, 이른바 마샬러를 제공한다.

그 중 내가 사용한 것은 org.springframework.oxm.jaxb.Jaxb2Marshaller 이다.

이 Jaxb2Marshaller 를 다음과 같이 MarshallingView 클래스의 생성자를 통하여 전달하도록 한다.

 

<
bean class="org.springframework.web.servlet.view.xml.MarshallingView">
    <constructor-arg>
        <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        </bean>
    </constructor-arg>
</bean>
 

 

 

그리고 이 Jaxb2Marshaller 가 어떤 클래스들을 XML로 변환할 것인지, 즉 XML로 변환할 작업 대상을 정해줘야 한다.
Spring에서 다뤄지는 사용자가 만든 클래스 모두가 XML로의 변환 대상은 아닐것이다.

그렇기 때문에 Jaxb2Marshaller가 XML로 변환할 클래스를 지정해줘야 한다.

 

위에서 Result와 InfoVO 클래스를 모두 XML로 변환해야 하기 때문에 Jaxb2Marshaller 클래스의 classesToBeBound 프로퍼티를 이용해서 
다음의 설정을 더 추가해준다

 
<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>
        </bean>
    </constructor-arg>
</bean>

 

 

 

이렇게 설정하면 MarshallingView 클래스는 변환하고자 하는 클래스가 지정되어 있는 Jaxb2Marshaller 클래스 객체를 생성자를 통해서 받아 XML 변환을 진행한다.

 

그럼 XML을 변환하는부분에 대해 조금 더 깊게 들어가보도록 한다.

위에서 정의한 Result 클래스와 Info 클래스의 소스를 보면 XML과 관련되어 사용된 어노테이션들이 있다.

이 부분에 대해 설명하고자 한다

 

 어노테이션

 설명

@XMLRootElement

 클래스를 XML로 변환할 때 XML의 Root Element 태그명을 정의하게 된다. Result 클래스를 보면 @XMLRootElement 어노테이션에 name="result" 로 설정되어 있다. 이것은 <result> 태그로 이 클래스를 XML로 변환할때의 Root Element로 한다는 의미이다. 즉 클래스를 XML로 변환할때 사용하게 될 XML의 Root 태그를 정의하는것이다

@XMLType

 클래스를 XML로 변환할때 이 XML의 이름과 namespace를 정의한다. (이 부분의 정확한 역할은 잘 모르겠다. 다만 프로젝트를 하면서 겪었던 경험을 말하자면 두개의 클래스 이상에서 동일한 설정을 할 경우 스프링 구동시 에러가 발생한다. 그래서 name와 namespace 설정을 클래스 이름과 패키지까지 명시한 클래스 이름으로 정의를 하면 한 클래스로 unique한 구성을 할 수 있기 때문에 두개 이상의 클래스가 동일한 설정이 되지 않는다)

propOrder는 XML 표현시 멤버 변수를 표현할 순서를 정의하게 된다. Result 클래스를 보면 propOrder={"flag", "message", "recordset"} 로 설정되어 있는데 이런 설정은 Result 클래스를 XML로 변환했을때 flag, message, recordset 멤버변수 순서대로 XML 태그를 변환하여 보여준다

@XMLElement

 클래스의 멤버변수를 XML로 변환하여 멤버변수에 들어있는 값을 표현할때 사용한다. @XMLElement 설정은 클래스의 멤버변수를 선언한 부분 또는 멤버변수의 setter 함수에 설정한다. Result 클래스를 보면 flag 변수의 setter 함수인 setFlag 함수에 @XMLElement 어노테이션에 name="flag" 로 설정되어 있다. 이것은 flag 변수의 값을 <flag> 태그의 value 값으로 표현한다는 의미다

@XMLElementWrapper

 다른 XML 변환 클래스를 감싸는 역할을 한다. Result 클래스에서 recordset 변수는 검색 결과를 담는 것이기 때문에 검색 결과 클래스 객체가 List 형태로 들어있다. 이를 XML로 표현할때는 반복적으로 표현하게 되는 데 이런 반복적으로 표현되는 XML을 감싸는 태그가 필요하게 된다. 그때 이 어노테이션을 사용하게 된다. Result 클래스에서는 <recordset>이란 태그를 사용하여 이런 반복적인 결과 출력을 감싸게 된다

@XMLElementRefs, @XMLElementRef

 List 같은 컬렉션 클래스를 XML로 변환할때 컬렉션 클래스에 들어가지는 클래스의 이름을 정의하게 된다. Result 클래스의 recordset 변수의 getter 함수인 getRecordset 함수를 보면 이 어노테이션이 정의되어 있다. 이 recordset의 내부에 InfoVO 클래스 객체가 들어가기 때문에 type에 InfoVO.class를 주게 된다. name은 record로 줌으로써 InfoVO 클래스를 XML로 표현할때 <record> 태그로 감싸게 된다.

 

 

이렇게 해서 표현되는 Result 클래스와 InfoVO 클래스를 XML로 표현한 예가 다음과 같다.

 

<
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>
 

 

하지만 지금과 같은 설정으로 할 경우 위와 같은 형태로 결과가 나오지 않는다.

정확하게 말하자면 XML로 결과가 나오지만 한줄로 쭉 표현이 되기 때문에 위와 같이 알아보기 좋은 형태로 나타나지 않는다.

 

그래서 위와 같이 알아보기 좋은 형태로 출력할려면 Jaxb2Marshaller 클래스의 marshallerProperties 프로퍼티에 다음의 설정이 필요한다

 

<
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>
        </list>
    </property>
</bean>
 

 

위와 같은 설정을 하면 XML을 알아보기 좋게 출력해준다.

이제 XML을 출력하기 위한 스프링 설정을 종합적으로 정리하면 다음과 같이 정리가 된다.

지금까지 XML 출력에 대해 정리했다. 다음에는 Json 출력에 대해 설명하겠다

 

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>