본문 바로가기

프로그래밍/Spring

Spring 3.1의 Controller에서 Custom Annotation을 파라미터로 이용했을때의 Map 클래스 받는 법

최근에 Spring 3.2와 마이플랫폼 연동을 하는 과정을 하면서 애로사항을 겪은 것이 있어서 이 포스트를 통해 정리하게 되었다. Controller에서 메소드의 파라미터에 Custom Annotation을 이용하여 사용자가 보낸 값을 받을때 Map으로 받는 것에 문제가 있었다. 이렇게 얘기하면 와 닿지가 않을수도 있을것 같아 예를 들어보도록 하겠다.

 

흔히 Controller의 메소드에서 파라미터를 사용할때 다음과 같이 상황에 맞는 적절한 어노테이션을 사용하게 될 것이다.

 

public String getList(@RequestParam(name="no") int no) throws Exception 

 

이렇게 파라미터를 받아다가 변수에 바로 설정하는 식의 방법을 많이 사용할것이다. 위의 예에서는 int 형으로 사용자가 보낸 값을 받았다. 근데 Spring 3.1로 넘어가면서 사용자가 만든 @requestParam 같은 어노테이션을 위의 예와 같이 int형이 아닌 Map 객체로 받을때 문제가 생긴다는 것이다. Map이 아닌 다른 클래스로 받는 것은 문제가 없었다. 그러나 Custom Annotation을 이용하여 Map으로 사용자가 보낸 값을 받을때는 생각한대로 동작하지 않았다. 원인을 구글링으로 검색해보니 Spring에서 사용자가 등록한 Custom HandlerArgumentResolver의 우선순위가 Spring에서 기본으로 제공하는 Map을 처리하는 HandlerMethodArgumentResolver보다 낮다는 것이다. 즉 Custom Annotation을 이용하여 Map으로 사용자가 보낸 데이터를 받을때 이 작업을 위해 등록한 Custom HandlerArgumentResolver보다 Spring에서 Map을 처리하는 HandlerMethodArgumentResolver를 먼저 만나기 때문에 사용자가 등록한 Custom HandlerArgumentResolver가 이를 처리할수 없기 때문이다. 이 부분을 눈으로 확인해보자. 다음의 그림은 이클립스에서 디버깅을 통해 보게된 Spring에서 로딩되는 HandlerArgumentResolver 들이다

 

 

여기서 봐야 할 것은 18번째의 MapMethodProcessor와 22번째의 MiplatformArgumentResolver이다. 마이플랫폼 연동과 관련하여 Custom Annotation을 이용하여 Map으로 받는 부분이 MiplatformArgumentResolver인데 이것이 Spring에서 Map으로 받는 작업을 해주는 MapMethodProcessor보다 앞순에 있다. 그래서 Map으로 받을때 MiplatformArgumentResolver를 통하여 Custom Annotation을 이용한 Map으로 받아야 하는것이 MapMethodProcessor가 가로채서 처리하기 때문에 문제가 발생한것이다. 그래서 이 순서를 바꿔줘야 한다. 즉 MiplatformArgumentResolver가 MapMethodProcessor보다 먼저 처리하게끔 해줘야 하는것이다.

 

이 부분을 구현하는 것은 Spring에서 제공하는 태그로만으로는 할 수가 없다. 그래서 Spring에서 사용하는 클래스를 직접 커스터마이징 해야 한다. 이 작업의 커스터마이징 포인트는 Spring 3.1에서 Spring MVC의 핵심 역할을 하는 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 클래스다. 이 클래스를 커스터마이징 해서 구현해야 한다. RequestMappingHandlerAdapter 클래스를 상속받은 새로운 클래스를 다음과 같이 작성한다.

 

import java.util.ArrayList;

import org.springframework.web.method.annotation.MapMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import com.terry.miplatform.common.miplatform.support.MiplatformArgumentResolver;

public class CustomRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

    @Override
    public void afterPropertiesSet() {
		// TODO Auto-generated method stub
		super.afterPropertiesSet();
	
		if(getArgumentResolvers() != null){
			ArrayList<HandlerMethodArgumentResolver> list = new ArrayList<HandlerMethodArgumentResolver>(getArgumentResolvers().getResolvers());
			MapMethodProcessor mapMethodProcessor = null;
			int size = list.size();
			for(int i = 0; i < size; i++){
				HandlerMethodArgumentResolver resolver = list.get(i);
				if(resolver instanceof MapMethodProcessor){
					mapMethodProcessor = (MapMethodProcessor)list.remove(i);
					break;
				}
			}
			
			if(mapMethodProcessor != null){
				for(int i = 1; i < size; i++){
					HandlerMethodArgumentResolver resolver = list.get(i);
					if(resolver instanceof MiplatformArgumentResolver){
						if(i + 1 < size){
							list.add(i+1, mapMethodProcessor);
						}else{
							list.add(i, mapMethodProcessor);
						}
						break;
					}
				}
			}
			
			setArgumentResolvers(list);
		}
    }

}

 

afterPropertiesSet 함수를 오버라이딩 해서 구현하게 되는데 코드가 하는 기능은 위에서 말한 순서대로 바꾸는 것이다. 즉 MiplatformArgumentResolver가 MapMethodProcessor보다 먼저 저장되게 하는 것이다. 그래서 이렇게 구현하면 다음의 그림과 같이 순서가 바뀌게 되는 것이다.

 

 

그림을 보면 21번째에 MiplatformArgumentResolver가 있고 22번째에 MapMethodProcessor가 있게 된다. 그래서 Map으로 받을때 먼저 MiplatformArgumentResolver를 보고 그 담에 MapMethodProcessor가 보게 되는 것이다. 이렇게 순서를 조정함으로써 사용자가 정의한 어노테이션을 먼저 처리하도록 유도하는 것이다. RequestMappingHandlerAdapter 클래스를 직접 쓴것이 아니라 상속을 받아 쓴 것이기 때문에 <mvc:annotation-driven /> 태그를 사용할 수 없다. 즉 수동으로 다음과 같이 bean을 설정해야 한다.

 

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
	<property name="order" value="0" />
</bean>

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
<bean id="conversion-service" class="org.springframework.format.support.FormattingConversionServiceFactoryBean" />

<!-- <mvc:message-converters> 태그의 messageConverter와 통합 -->
<util:list id="messageConverters">
	<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
	<bean class="org.springframework.http.converter.StringHttpMessageConverter">
		<property name="writeAcceptCharset" value="false" />
	</bean>
	<bean class="org.springframework.http.converter.ResourceHttpMessageConverter" />
	<!-- 
	기존에 사용하는 org.springframework.http.converter.xml.SourceHttpMessageConverter 와
	org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter 와
	org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter 와
	org.springframework.http.converter.json.MappingJacksonHttpMessageConverter 는
	
	org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter를 사용함으로써
	동적으로 추가된다(AllEncompassingFormHttpMessageConverter 클래스 소스를 보면 SourceHttpMessageConverter 클래스는 필수로 들어가고
	나머지 3개의 클래스는 라이브러리 유무에 따라 동적으로 MessageConverter를 추가한다)
	 -->
	<bean class="org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter" />
	
	<!-- 
	<bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter" />
	<bean class="org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter" />
	<bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter" />
	<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
	-->

	<!-- rome 라이브러리 존재시 -->
	<!--
	<bean class="org.springframework.http.converter.feed.AtomFeedHttpMessageConverter" />
	<bean class="org.springframework.http.converter.feed.RssChannelHttpMessageConverter" />
	-->
</util:list>

<bean class="com.terry.miplatform.common.CustomRequestMappingHandlerAdapter">
	<property name="webBindingInitializer">
		<bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
			<property name="validator" ref="validator" />
			<property name="conversionService" ref="conversion-service" />
		</bean>
	</property>
	<property name="messageConverters" ref="messageConverters"/>

	<!-- <mvc:argument-resolvers> 태그 존재시 -->
	<property name="customArgumentResolvers">
		<util:list>
			<bean class="com.terry.miplatform.common.miplatform.support.MiplatformArgumentResolver" />
		</util:list>
	</property>
	 
	<!-- <mvc:return-value-handlers> 태그 존재시 -->
	<!--
	<property name="customReturnValueHandlers">
		<util:list>
		</util:list>
	</property>
	-->
</bean>

<bean class="org.springframework.web.servlet.handler.MappedInterceptor">
	<constructor-arg index="0">
		<null />
	</constructor-arg>
	<constructor-arg index="1">
		<bean class="org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor">
			<constructor-arg index="0" ref="conversion-service" />
		</bean>
	</constructor-arg>
</bean>

<bean class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver">
	<property name="messageConverters" ref="messageConverters"/>
	<property name="order" value="0"/>
</bean>

<bean class="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver">
	<property name="order" value="1"/>
</bean>

<bean class="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver">
	<property name="order" value="2"/>
</bean>

 

XML로 된 설정을 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 클래스를 상속받은 com.terry.miplatform.common.CustomRequestMappingHandlerAdapter 클래스를 이용하여 기존의 RequestMappingHandlerAdapter 클래스가 하던 셋팅을 대신 하고 있다. 그리고 사용자 어노테이션을 이용해서 Map 객체를 받아오는 작업을 처리하는 com.terry.miplatform.common.miplatform.support.MiplatformArgumentResolver를 customArgumentResolvers 프로퍼티에 등록해줌으로써 사용자 어노테이션을 이용해 Map 객체로 받아오는 부분을 하고 있다.