[Spring MVC] DispatcherServlet의 처리 과정 - HandlerMapping

박상혁·2023년 4월 24일

Spring MVC

목록 보기
1/2

개요

Spring MVC의 DispatcherServlet이 어떻게 요청을 받고 응답을 처리하는지 그 내부 과정을 알아내기 위해 해당 포스팅을 작성하였습니다.

목차

  1. DispatcerServlet의 work flow 설명
  2. HandlerMapping의 역할 간단 설명
  3. DispatcherServlet 초기화
  4. .doService(), .doDispatch()
  5. DispatcherServlet.getHandler()
  6. RequestMappingHandlerMapping 클래스 상속도
  7. AbstractHandlerMapping.getHandler()
  8. AbstractHandlerMethodMapping.getHandlerInternal(request)
  9. AbstractHandlerMethodMapping.lookupHandlerMethod()

본문

본 코드 분석은 Spring Web MVC 2.7.9 기준으로 작성하였습니다.

사전 지식

Spring MVC는 이름 그대로 MVC 아키텍쳐를 사용합니다.

  • Model : 정보가 담김

  • View : 화면 출력 담당

  • Controller : 제어 로직

  • MVC 아키텍쳐는 Front Controller패턴을 사용합니다.
    클라이언트의 요청을 먼저 받고 공통 작업 수행
    이후, 나머지 세부 로직을 세부 컨트롤러로 위임
    DispatcherServlet가 프론트 컨트롤러입니다.

DispatcherServlet flow

public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";
public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter";
@Nullable
private List<HandlerMapping> handlerMappings;
@Nullable
private List<HandlerAdapter> handlerAdapters;
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;

기본 default 전략 초기화

protected void initStrategies(ApplicationContext context) {
	initMultipartResolver(context);
	initLocaleResolver(context);
	initThemeResolver(context);
	initHandlerMappings(context);
	initHandlerAdapters(context);
	initHandlerExceptionResolvers(context);
	initRequestToViewNameTranslator(context);
	initViewResolvers(context);
	initFlashMapManager(context);
}
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");
		}
	}

	for (HandlerMapping mapping : this.handlerMappings) {
		if (mapping.usesPathPatterns()) {
			this.parseRequestPath = true;
			break;
		}
	}
}

doService()

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
	logRequest(request);
    ...
    try {
		doDispatch(request, response);
	}
    ...

doDispatch()

DispatcherServlet의 doDispatch() 메서드 중 중간부분입니다.
getHadler() 메서드를 통해 요청값을 분석한 후 결과 Handler를 반환하려고 합니다.
gethandler()부터 자세히 파해쳐봅니다.

try {
	processedRequest = checkMultipart(request);
	multipartRequestParsed = (processedRequest != request);
	
    // handler 가져옴
	mappedHandler = getHandler(processedRequest);
	if (mappedHandler == null) {
		noHandlerFound(processedRequest, response);
		return;
	}

	HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())
    ...
}
  • DispatcherServlet.getHandler()
    위에서 보다시피 DispatcherServlet은 handlerMapping 전략들을 List 형태로 담아두고 있습니다.
    반복문을 통해 전략 타입의 메서드 getHandler()를 호출합니다.
  • Annotation을 통해 Handler를 만들었기 때문에 RequestMappingHandlerMapping.getHandler() 호출
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	if (this.handlerMappings != null) {
		for (HandlerMapping mapping : this.handlerMappings) {
			HandlerExecutionChain handler = mapping.getHandler(request);
			if (handler != null) {
				return handler;
			}
		}
	}
	return null;
}
  • RequestMappingHandlerMapping 클래스 상속도

다음과 같이
HandlerMapping --구현--> AbstractHandlerMapping --상속--> AbstactHandlerMethodMapping --상속--> RequestMappingInfoHandlerMapping --상속--> RequestMappingHandlerMapping
로 구현되어 있습니다.
이렇게 추상 클래스로 구현되어 상속이 이어져 최종적으로 RequestMappingHandlerMapping이 만들어졌습니다.
이중 getHander()가 구현된 곳은 AbstractHandlerMapping 클래스입니다.

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware

public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo>

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered, BeanNameAware {
    ...
}
  • AbstractHandlerMapping.getHandler()

    Look up a handler for the given request, falling back to the default
    주어진 request에 맞는 handler를 찾아주는 역할을 함

위의 AbstractHandlerMapping 추상클래스 내부의 메서드 입니다.

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	Object handler = getHandlerInternal(request);
	if (handler == null) {
		handler = getDefaultHandler();
	}
	if (handler == null) {
		return null;
	}
    ...
}

우선 가장 첫째줄인, getHandlerInternal()부터 알아보겠습니다.

  • AbstractHandlerMethodMapping.getHandlerInternal(request)

    Look up a handler method for the given request.
    request 요청 정보를 토대로 handler method를 실제로 찾습니다.

AbstractHandlerMapping 클래스를 상속한 AbstractHandlerMethodMapping<T> 추상클래스에 선언되어 있습니다.

  1. initLookupPath(request)를 통해 사용자가 요청한 URL을 파악합니다.
  2. lookupHandlerMethod(lookupPath, request) 메서드를 사용해 URL과 request 요청 정보를 토대로 Handler를 찾아냅니다.
@Override
@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
	String lookupPath = initLookupPath(request);
	this.mappingRegistry.acquireReadLock();
	try {
		HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
		return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
	}
	finally {
		this.mappingRegistry.releaseReadLock();
	}
}
  • AbstractHandlerMethodMapping.lookupHandlerMethod()
  1. pathLookup이라는 MultiValueMap 자료구조에 URL과 그에 맞는 Handler가 key value 형태로 저장되어 있습니다.
    (만일 Controller에 GET으로 설정한 /board와 POST로 설정한 /board/{id}를 만들었다면
    value값으로 GET /board, POST /board/{id}가 각각 저장되어 있습니다.)
    이를 가져와서 List<T> 형태인 directPathMatches에 담습니다.
    (List인 이유는, 한 URL에 여러 Method가 있을 수 있기 때문입니다.)

  2. addMatchingMappings(directPathMatches, matches, request) 메서드를 통해 위에서 설정한 값을 토대로 진짜 Handler Method를 찾아낸 후 matches에 추가하게 됩니다.

  3. 해당 Handler Method가 bestMatch인지 확인 후 이를 return합니다.

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
	List<Match> matches = new ArrayList<>();
	List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
    if (directPathMatches != null) {
		addMatchingMappings(directPathMatches, matches, request);
	}
    ...
    return bestMatch.getHandlerMethod();
}
@Nullable
public List<T> getMappingsByDirectPath(String urlPath) {
	return this.pathLookup.get(urlPath);
}

이를 통해 최종적으로 DispatcherServlet.getHandler() 메서드는
위의 과정을 통해서 Handler Method가 반환됩니다.

요약

참조

SpringMVC - Spring MVC 동작원리 - 4(DispatcherServlet의 ResponseBody 응답 과정, HandlerMapping, HandlerAdapter)
[spring] HandlerMapping, HandlerAdapter, HandlerInterceptor

profile
개발 노트

1개의 댓글

comment-user-thumbnail
2024년 1월 1일

글 잘 읽었습니다.
궁금한 점이 있는데
"이를 통해 최종적으로 DispatcherServlet.getHandler() 메서드는
위의 과정을 통해서 Handler Method가 반환됩니다."
이라고 마지막부분에서 Handler Method라 함은 @Controller 내부의 메서드를 의미하는 건가요?

답글 달기