본문 바로가기

Tech/Spring

[Spring MVC] HandlerMapping

 

핸들러 매핑은 HTTP 요청 정보를 이용해서 컨트롤러를 찾아주는 기능을 수행한다. DispatcherServlet이 등록된 HandlerMapping 전략들에게 HttpServletRequest를 전달하면서 매칭되는 오브젝트를 찾는다.

public interface HadlerMapping {
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
  • HandlerMapping은 DispatcherServlet에 의해 초기화된다.
  • HandlerMapping은 항상 HandlerExecutionChain을 통해 실행된다.
  • HandlerExecutionChain은 AbstractHandlerMapping 클래스에 의해 생성된다.
  • 기본 HandlerMapping 구현체인 BeanNameUrlHandlerMapping은 AbstractUrlHandlerMapping을 상속한다.
  • 기본 HandlerMapping 구현체인 RequestMappingHandlerMapping은 AbstractHandlerMethodMapping을 상속한다.

 

HandlerMapping의 초기화

HandlerMapping은 DispatcherServlet의 onRefresh()가 호출되는 시점에 초기화가 이뤄진다.

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

구체적으로 initHadlerMappings()에 세부로직이 포함되어 있다.

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    if (this.detectAllHandlerMappings) {
        // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default HandlerMapping later.
        }
    }

    // Ensure we have at least one HandlerMapping, by registering
    // a default HandlerMapping if no other mappings are found.
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}

detetectAllHandlerMappings가 true인 경우 모든 HandlerMapping을 초기화한다. (기본값은 true)

  • BeanFactoryUtils.beansOfTypeIncludingAncestors()을 통해 빈으로 등록된 모든 HandlerMapping을 가져와서 hanlderMappings라는 ArrayList 멤버 변수에 저장한다.
  • AnnotationAwareOrderComparator.sort()를 통해 초기화 후 우선순위 기준으로 정렬한다.

detetectAllHandlerMappings가 false인 경우 하나의 HandlerMapping만 초기화 한다.

  • context.getBean() 메소드를 이용해 “handlerMapping”이란 이름을 갖는 bean을 가져온다.
  • Collections.singletonList()으로 싱글톤으로 초기화한다.
  • 여전히 handlerMappings가 null이라면 getDefaultStrategies() 메서드를 통해 default로 초기화한다.

 

HandlerMapping의 전략

스프링이 제공하는 핸들러 매핑 전략은 5가지다.

BeanNameUrlHandlerMapping

빈의 이름에 들어있는 URL을 HTTP 요청의 URL과 비교해서 일치하는 빈을 찾아준다. 가장 직관적이고 사용하기 쉬운 핸들러 매핑 전략이다.

빈 이름에는 ANT패턴이라고 불리는 *, **, ? 를 이용한 패턴을 넣을 수 있다.

<!-- hello로 시작하는 경로 매핑-->
<bean name="/hello*" class="HelloController" />

<!-- **는 하나 이상의 경로 매핑 -->
<bean name="/root/**/sub" class="SubController" />

컨트롤러의 개수가 많아질수록 URL 정보가 XML이나 어노테이션에 분산되어 파악하기 어렵기 때문에, 복잡한 애플리케이션에서는 잘 사용하지 않는다.

ControllerBeanNameHandlerMapping

BeanNameUrlHandlerMapping과 유사하지만 빈 이름을 URL 형태로 짓지 않아도 된다는 것이 차이점이다. 빈 이름 앞에 자동으로 / 이 붙여져 URL에 매핑된다.

<!-- /hello에 매핑 -->
<bean name="hello" class="HelloController" /> 

ControllerClassNameHandlerMapping

빈의 클래스 이름을 URL에 매핑해주는 매핑 클래스. 기본적으로 클래스 이름을 모두 사용하지만 클래스 이름이 Controller로 끝날 경우 Controller를 뺀 나머지 이름을 URL에 매핑해준다.

public class HelloController implements Controller{ // /hello에 매핑
	// ...
}

SimpleUrlHandlerMapping

URL과 컨트롤러 매핑정보를 한곳에 모아놓을 수 있는 전략이다. 매핑정보는 SImpleUrlHandlerMapping 빈의 프로퍼티에 넣어준다.

디폴트 핸들러 매핑 전략이 아니기도 하고 프로퍼티에 매핑정보를 직접 넣어줘야 하므로 SimpleUrlHandlerMapping 빈을 등록해야 사용할 수 있다.

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <props>
            <prop key="/hello">helloController</prop>
            <prop key="/sub/*">myController</prop>
            <prop key="deep/**/sub">subController</prop>
        </props>
    </property>
</bean>
 
<bean id="helloController" .../>
<bean id="myController" .../>
<bean id="subController" .../>

매핑정보가 한 곳에 모여있기 때문에 URL 관리가 편하다는 장점이 있지만, 매핑할 컨트롤러 빈의 이름을 직접 넣어줘야하기 때문에 오타 등의 오류가 발생할 가능성이 있다.

DefaultAnnotationHandlerMapping

@RequestMapping 어노테이션을 이용해 매핑하는 전략이다. 클래스는 물론 메서드 단위로도 URL을 매핑할 수 있다.

또한 URL 뿐 아니라, GET/POST 와 같은 HTTP 메소드 정보, 심지어는 파라메터와 HTTP 헤더정보까지 매핑에 활용할 수 있다.

하지만 매핑 어노테이션의 사용정책과 작성 기준을 잘 만들어 두지 않으면, 개발자마다 제멋대로 매핑방식을 적용해서 매핑정보가 지저분해지고 관리하기 힘들어질 수도 있으니 주의해야 한다.

 

HandlerMapping 우선순위

여러 구현체의 HandlerMapping들은 우선순위를 가지고 있다. 우선순위가 높은 순서대로 각 구현체 전략에 따라 요청을 처리할 Handler를 찾아보고, 찾을 수 없으면 다음 우선순위가 높은 HandlerMapping이 찾는 방식으로 동작한다.

Spring에서 자동으로 추가하는 HandlerMapping의 우선순위를 살펴보기 위해 다음과 같은 테스트 코드를 작성하고 돌려보았다.

@Autowired
ApplicationContext context;

@Test
void test() {
Map<String,HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
            context,HandlerMapping.class,true,false);

    matchingBeans.forEach((k, v) -> System.out.printf("order:%s %s=%s%n",
            ((Ordered) v).getOrder(),
            k, v.getClass().getSimpleName()));
}

실행 결과는 아래와 같다.

RequestMappingHandlerMapping > BeanNameUrlHandlerMapping > RouterFunctionMapping > WelcomPageHandlerMapping > SimpleUrlHandlerMapping 순서로 우선순위가 잡혀있는 걸 확인했다.

다음과 @Configuration을 통해 addViewController() 코드를 작성하면 어떻게 될까?

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
        registry.addViewController("/user").setViewName("user/form");
    }
}

SimpleUrlHandlerMapping타입의 viewControllerHandlerMapping이 추가되었다.

setOrder()를 통해 우선순위를 지정할 수도 있었다. Ordered.HIGHEST_PRECEDENCE의 경우에는 Integer.MIN_VALUE를 리턴하기 때문에 가장 최우선순위로 지정 가능하다.

반대로, Ordered.LOWEST_PRECEDENCE의 경우에는 Integer.MAX_VALUE를 리턴하기 때문에 가장 마지막에 거치는 핸들러매핑이된다.

 

DefaultAnnotationHandlerMapping는 어디있지?

앞서 스프링에서 제공하는 핸들러매핑 전략으로 DefaultAnnotationHandlerMapping가 있다고 언급했다. 하지만 해당 핸들러매핑은 찍히지 않았다.

그렇다면 현재 @RequestMapping을 통한 매핑은 누가 처리하고 있는걸까? 확인을 위해 DispatcherServlet의 getHandler()에 다음과 같이 브레이크를 걸어두고 디버깅을 시도해봤다.

디버깅 결과 RequestMappingHandlerMapping이 처리하고 있음을 확인할 수 있었다.

이에 대해 구글링해본 결과, 스프링 3.1 이후부터 DefaultAnnotationHandlerMapping이 deprecated되면서 RequestMappingHandlerMapping로 대체되었고, 기본 HandlerMapping이 되었다고 한다.

위에서 확인했듯이 별다른 지정이 없다면 RequestMappingHandlerMapping는 가장 Order값이 0으로 가장 먼저 사용되는 HandlerMapping이다.

 

 

참고

반응형