커스텀 ArgumentResolver 등록하기

개요

스프링 사용시 컨트롤러에 들어오는 파라미터나 리턴타입을 나한테 맞게 가공해서 사용하고 싶을 때가 있다.. 기존에는 WebArgumentResolver를 구현해서 AnnotationMethodHandlerAdapter에 등록을 해야 했는데, 스프링 3.2버전 부터는 deprecated 된 상태다. 지금은 RequestMappingHandlerAdapter 클래스가 담당하는데, 이 클래스에서는 WebArgumentResolver가 HandlerMethodArgumentResolver로 바뀌었다.

예제

request 파라미터 수집을 Map 형태로 하는 클래스를 만들어서 등록해보자. 먼저 Spring Project로 생성한 뒤, 파라미터 수집하는 클래스를 생성하자.



ParamCollector.java

package com.tistory.jekalmin;

import java.util.HashMap;
import java.util.Map;

public class ParamCollector {

    Map<String, String> map = new HashMap<String, String>();

    public String get(String key){
        return map.get(key);
    }

    public void put(String key, String value){
        map.put(key, value);
    }

    public String toString() {
        return map.toString();
    }


}

여기서 주의해야 할 점은 Map을 상속 받거나 구현하면, 다른 ArgumentResolver로 갈 수도 있다. 다음은 ArgumentResolver를 구현해서 DispatcherServlet에 등록해주자.



ParamCollectorArgumentResolver.java

package com.tistory.jekalmin;

import java.util.Iterator;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class ParamCollectorArgumentResolver implements HandlerMethodArgumentResolver{

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // TODO Auto-generated method stub
        return ParamCollector.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) throws Exception {
        // TODO Auto-generated method stub
        ParamCollector collector = new ParamCollector();
        for(Iterator<String> iterator = webRequest.getParameterNames(); iterator.hasNext();){
            String key = iterator.next();
            collector.put(key, webRequest.getParameter(key));
        }
        return collector;
    }

}


mvc-config.xml (dispatcherServlet 설정파일)

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.tistory.jekalmin"/>


    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="com.tistory.jekalmin.ParamCollectorArgumentResolver"></bean>
        </mvc:argument-resolvers>
    </mvc:annotation-driven>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <!-- Example: a logical view name of 'showMessage' is mapped to '/WEB-INF/jsp/showMessage.jsp' -->
            <property name="prefix" value="/WEB-INF/view/"/>
            <property name="suffix" value=".jsp"/>
    </bean>

</beans>

이제 테스트를 위한 컨트롤러를 생성한다.



TestController.java

package com.tistory.jekalmin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/test")
public class TestController {

    @RequestMapping("/index")
    public void index(ParamCollector collector){
        System.out.println(collector);
    }
}



다음 http://localhost:8080/test/index?aaa=bbb&ccc=ddd로 호출을 해결과는 다음과 같다.

{aaa=bbb, ccc=ddd}

결론

ArgumentResolver를 활용하면 파라미터 수집 이 외에도 다양하게 커스터마이징 할 수 있다. 어떻게 활용하느냐는 여러분에게 달려있다.

ApplicationContext를 써야하는 이유

개요

필자는 스프링을 사용할 때, 웹에 관련된 설정들은 DispatcherServlet에 넣고, 순수 자바 설정들은 ApplicationContext에 넣으라고 배웠다. 그러나 모든 설정을 DispatcherServlet안에 넣어도 잘 작동하는데, 왜 굳이 ApplicationContext를 써야 할까?

예제

스프링 시큐리티 작동 방식을 예로 들어보자. 스프링 시큐리티는 필터를 이용해서 사용자의 인증과 권한 여부를 체크한다. 만약 ApplicationContext를 사용하지 않는다면, 스프링 시큐리티의 설정 파일들을 DispatcherServlet에 정의해야한다. 그렇다면 문제는 무엇일까?

이것은 톰캣 같은 WAS(Web Application Server)가 생성하는 컴포넌트 순서가 중요하다. web.xml에 등록된 컴포넌트가 생성되는 순서는 Listener - Filter - Servlet 순서이다. 심지어 서블릿은 load-on-startup 같은 설정을 해주지 않으면 호출되기 전까지 생성되지도 않는다. 근데 스프링 시큐리티에서 사용할 설정을 서블릿인 DispatcherServlet 안에 넣을 경우, WAS에서 필터를 생성할 때는 아직 DispatcherServlet이 생성되지 않았으므로 시큐리티에 관련된 필터를 생성할 수 없다.

그래서 필터를 생성하기 전에 스프링 관련 설정을 적용하려면 listener에 등록해야 한다.

그렇다면 스프링에서 제공하는 CharacterEncodingFilter는 어떨까? 인코딩 필터는 스프링에서 제공하긴 하지만 encoding 변수만 설정해주면 되기 때문에 web.xml 안에서 설정이 가능하다. 그래서 스프링과는 별도로 작동할 수 있었다.

결론

ApplicationContext를 안썼을 때 문제점이 더 많을 수도 있다. 필자는 이것을 쓰는 것이 싫은 것이 아니다. 과연 어떤 경우에 ApplicationContext가 꼭 필요한가 궁금했었는데, 스프링에서 설정을 기반으로 필터를 등록해야 한다면, ApplicationContext를 Listener로 올리는 것은 선택이 아닌 필수이다.


ApplicationContext vs DispatcherServlet 2 : 컨텍스트 간의 bean 공유?

ApplicationContext와 DispatcherServlet은 부모와 자식 관계로 연결되어 있는데, 왜 component-scan은 양쪽 다 해야할까? 두 컨텍스트는 같은 빈들을 가리키는 걸까? 밑에 예제에서는 ApplicationContextHolder 라는 클래스를 만들어서 두 컨텍스트를 가지고 있다가, 두 컨텍스트가 가지고 있는 빈의 인스턴스가 같은지 비교해보았다.


application-context.xml


<?xml version="1.0" encoding="UTF-8"?>


<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.tistory"></context:component-scan>

    

    <bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

<bean class="com.tistory.jekalmin.dao.TestJdbcTemplate"/>

</beans>

TestJdbcTemplate은 applicationContext에만 등록하고, dispatcherServlet에는 등록하지 않기 위해 어노테이션을 사용하지않고 application-context.xml에서 bean으로 등록하였다.


spring-servlet.xml


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.tistory.jekalmin"/>

<context:annotation-config/>

<bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

<bean class="com.tistory.jekalmin.dao.TestJdbcTemplate"/>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="/WEB-INF/view/"/>

<property name="suffix" value=".jsp"/>

</bean>

</beans>

application-context.xml과 spring-servlet.xml 양쪽에 component-scan을 하였다.


ApplicationContextHolder.java


package com.tistory.jekalmin.common;


import java.util.ArrayList;

import java.util.List;


import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;


public class ApplicationContextHolder implements ApplicationContextAware{

public static List<ApplicationContext> list = new ArrayList<ApplicationContext>();

@Override

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

list.add(ctx);

}


}

두 컨텍스트에서 ApplicationContextHolder 빈을 각각 생성할 때, ApplicationContextHolder 클래스에서 컨텍스트에 접근할 수 있도록 컨텍스트를 등록하였다.


TestDao.java


package com.tistory.jekalmin.dao;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

@Repository

public class TestDao {

@Autowired

public TestJdbcTemplate jdbcTemplate;

}

TestDao는 어노테이션을 이용해 등록했으므로 양쪽 컨텍스트에서 component-scan을 이용해 등록될 것이다.


TestJdbcTemplate.java


package com.tistory.jekalmin.dao;

public class TestJdbcTemplate {

}

TestJdbcTemplate은 위에 application-context.xml에서만 등록하였다.


TestController.java


package com.tistory.jekalmin.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.ApplicationContext;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import com.tistory.jekalmin.common.ApplicationContextHolder;

import com.tistory.jekalmin.dao.TestDao;

@Controller

public class TestController {

@Autowired

private TestDao testDao;

@RequestMapping("/")

public void index(){

for(ApplicationContext ctx : ApplicationContextHolder.list){

System.out.format("[%s] testController hashCode : %d\n", ctx.getId(), ctx.getBean("testController").hashCode());

System.out.format("[%s] testDao hashCode : %d\n", ctx.getId(), ctx.getBean("testDao").hashCode());

System.out.format("[%s] testJdbcTemplate hashCode : %d\n", ctx.getId(), testDao.jdbcTemplate.hashCode());

}

}

}

이제 url을 호출하여 테스트 해보자. 테스트 내용은 기존에 ApplicationContext에서 가져온 빈과 DispatcherServlet에서 가져온 빈의 hashCode를 비교해 인스턴스가 같은지 다른지 확인하였다.

url 호출 결과는 아래와 같다.


[org.springframework.web.context.WebApplicationContext:] testController hashCode : 631271355

[org.springframework.web.context.WebApplicationContext:] testDao hashCode : 1666005388

[org.springframework.web.context.WebApplicationContext:] testJdbcTemplate hashCode : 1060031077

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testController hashCode : 319851274

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testDao hashCode : 896263012

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testJdbcTemplate hashCode : 1060031077

결과를 보면 testJdbcTemplate만 hashCode가 같고, testController와 testDao는 다른 것을 확인 할 수 있다.

결론 : ApplicationContext와 DispatcherServlet에서 component-scan을 이용해서 등록된 빈들은 다른 인스턴스이다. 다만 ApplicationContext에만 등록된 빈의 경우 DispatcherServlet에서 검색해보고 없으면 부모인 ApplicationContext에서 가져온다.

ApplicationContext vs DispatcherServlet : 컨텍스트 간의 관계

웹쪽 자원을 관리하는 DispatcherServlet과 그 외의 자원을 관리하는 ApplicationContext는 어떤 관계가 있을까? 예제에서는 DispatcherServlet과 ApplicationContext의 정보를 가져오기 위해 ApplicationContextAware를 구현한 클래스를 양쪽에 등록하고, 두 컨텍스트의 정보를 출력해봤다.

ApplicationContextHolder.java


package com.tistory.jekalmin.common;


import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;


public class ApplicationContextHolder implements ApplicationContextAware{

@Override

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

System.out.println("================================================");

System.out.println("applicationContext : " + ctx.getId());

System.out.println("applicationContext hashCode : " + ctx.hashCode());

ApplicationContext parent = ctx.getParent();

if(parent != null){

System.out.println("parent : " + parent.getId());

System.out.println("parent hashCode : " + parent.hashCode());

}

System.out.println("================================================");

}


}



web.xml


<context-param>

        <param-name>contextConfigLocation</param-name>

        <param-value>

        classpath:spring/application-context.xml

        </param-value>

    </context-param>


    <listener>

        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

    </listener>

    

    <servlet>

        <servlet-name>dispatcherServlet</servlet-name>

        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <init-param>

            <param-name>contextConfigLocation</param-name>

            <param-value>classpath:spring/spring-servlet.xml</param-value>

        </init-param>

        <load-on-startup>1</load-on-startup>

    </servlet>


결과는 다음과 같다.


================================================ applicationContext : org.springframework.web.context.WebApplicationContext: applicationContext hashCode : 966881334 ================================================ ================================================ applicationContext : org.springframework.web.context.WebApplicationContext:/dispatcherServlet applicationContext hashCode : 1233412676 parent : org.springframework.web.context.WebApplicationContext:/SpringTest parent hashCode : 966881334 ================================================


applicationContext 먼저 생성되고, dispatcherServlet은 applicationContext를 부모로 가지고 있는다.

여러개의 Spring xml 설정파일, ApplicationContext는 몇개인가

보통 스프링 프로젝트를 만들 때 xml 기반으로 ApplicationContext를 생성하는 경우가 많다. 그리고 스프링 설정파일들도 ContextLoaderListener 라는 리스너에 여러개 등록할 수 있는데, 과연 이 경우에 컨텍스트가 여러개 생셩되는지, 하나의 컨텍스트가 생성되는 지 궁금했다. 

결론부터 말하자면 리스너에 아무리 많은 xml 파일들을 등록해도 하나의 ApplicationContext만 올라간다. 

아래 예제에서는 bean을 생성해준 컨텍스트 정보를 알 수 있도록 ApplicationContextAware를 구현한 클래스를 양쪽 xml에 등록하고 컨텍스트 정보를 출력해서 비교하였다.


web.xml


<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>

    classpath:spring/application-context.xml

    classpath:spring/spring-security.xml

    </param-value>

</context-param>

application-context.xml


<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.tistory"></context:component-scan>

    <bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

</beans>


spring-security.xml


<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    

    <bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>


</beans>


ApplicationContextHolder.java


package com.tistory.jekalmin.common;


import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;


public class ApplicationContextHolder implements ApplicationContextAware{

@Override

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

System.out.println("applicationContext : " + ctx);

System.out.println("applicationContext hashCode : " + ctx.hashCode());

}


}


결과는 다음과 같다.


applicationContext : Root WebApplicationContext: startup date [Tue Sep 30 10:27:17 EDT 2014]; root of context hierarchy

applicationContext hashCode : 12545140

applicationContext : Root WebApplicationContext: startup date [Tue Sep 30 10:27:17 EDT 2014]; root of context hierarchy

applicationContext hashCode : 12545140





두개의 다른 xml에서 ApplicationContextAware를 구현한 클래스를 등록하고 hashCode를 출력하였는데, 같은 객체임을 알 수 있다.