본문 바로가기

프로그래밍/Spring

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

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에 대한 내용을 다루도록 하겠다