Framework/Spring

[IBAS] Spring MVC의 Handler Mapping 동작 원리 (1) 이론 편

MINGYUM 2022. 2. 14. 18:18

지난 포스팅에서 menuId에 대한 Controller 통합을 위해 BoardService Interface를 설계하고, 하나의 BoardController에서 menuId에 맞는 Service 객체를 반환하는 Mapper 클래스를 만들고 

 

별 난리를 떨었다 

 

결론은 실패였다. 

@RequestBody로 받을 수 있는 객체가 제너릭이 불가능한 점, 

java - Interfaces and @RequestBody - Stack Overflow

 

Interfaces and @RequestBody

I'm currently working on a project which allows users to book (via the web) the use of a chosen resource for a given period of time. In this program I am trying to keep with Spring's philosophy (an...

stackoverflow.com

사실 가능하지만, 그렇게 구현하려면 BoardController에 대한 인터페이스를 또 만들어야 한다고 생각하여,

그것까지는 배보다 배꼽이 더 큰 격이 되는 것 같아 포기했다.

 

 

더불어, 백엔드 팀장님께서

BoardController를 하나로 합침으로써 더더욱 확장된 기능이 있는 (Controller단에서 CRUD 이외의 추가적인 메소드를 만들어야 하는 경우) MenuType이 들어왔을 때 BoardService Interface 에서 제공된 선언 이외의 다른 메소드를 구현해야하고, 이에 의해서 컨트롤러 단에서 그 메뉴 타입만을 위한 함수를 만들어야하는 일이 생긴다고 조언햇다..

 

그래서 결국엔 브랜치 다시 파서 만들기 시작

 

Mapper를 이용해 만든 BoardService를 반환해 구현한 것처럼 비슷하게, 

Handler Adapter라는 놈에게 내가 직접 커스터마이징한 Handler Mapping을 제공하면 된다 .

 

그 전에 Reference 공부부터 하고 오자. 

 

https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/mvc.html

 

17. Web MVC framework

@RequestMapping(method = RequestMethod.POST) public String processSubmit(@ModelAttribute("pet") Pet pet, Model model, BindingResult result) { … } Note, that there is a Model parameter in between Pet and BindingResult. To get this working you have to reor

docs.spring.io


 

현재 @RestController를 사용하고 있으므로,

목적을 달성하기 위해 Handler Mapping에서 Controller를 찾아 반환하는 방식을 바꾸면 된다. 

 

Dispatcher Servelt에서 doService의 내부에서 호출되는 doDispatch를 통해 getHandler함수가 호출되고,

getHandler 함수에서 List 형태의 handlermappings을 가져와 for문을 돌면서 

HandlerMapping 타입의 mapping들에 대한 getHandler를 호출해서 HandlerExecutionChain타입의 객체를 반환하여 

 

@Override
	@Nullable
	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}

		// Ensure presence of cached lookupPath for interceptors and others
		if (!ServletRequestPathUtils.hasCachedPath(request)) {
			initLookupPath(request);
		}

		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		if (logger.isTraceEnabled()) {
			logger.trace("Mapped to " + handler);
		}
		else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
			logger.debug("Mapped to " + executionChain.getHandler());
		}

		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = getCorsConfiguration(handler, request);
			if (getCorsConfigurationSource() != null) {
				CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
				config = (globalConfig != null ? globalConfig.combine(config) : config);
			}
			if (config != null) {
				config.validateAllowCredentials();
			}
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}

 

AbstractHandlerMapping에 위치한 getHandler 메소드의 정의이다. 

 

@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();
		}
	}

getHandlerInternal은 getHandler 함수에서 호출되어, lookupHandlerMethod에서 url에 해당하는 적절한 handlerMethod를 가져와  return한다. 이 handlermethod가 우리가 구현한 Controller 함수이다.  

 

 

이제 여기서 핵심, 

내가 받은 request로 직접 menuId로 menuRepository에서 메뉴를 조회한 다음

해당하는 컨트롤러에 매핑하려면 어디 함수를 건드려야할까?  

 

lookupHandlerMethod 를 좀 더 보자. 

 

List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);

urlPath에 매핑되는 handler method를 가져와 directPathMatches 리스트에 저장한다. 

 

if (directPathMatches != null) {
    addMatchingMappings(directPathMatches, matches, request);
}

리스트로 가져와졌다면 addMatchingMappings 함수를 이용해서,

current request와 리스트의 각 mapping이 일치할 경우 mapping을 반환하여 match에 담고,

일치되었을 때 가져와진 match를 matches, 즉 위에서 정의한 matches List에 담는다. 

(담을 때, Match 객체를 생성한다. )

 이때 mapping 저장소에서 현재 request와 일치하는 mapping의 Registration을 가져와 Match를 생성할 때 이용해준다. 참고로, MappingRegistration 은 mapping, handlerMethod, directPaths, mappingName 등의 정보가 들어있는 클래스이며 Match 객체를 생성해 matches 리스트에 담아줌으로써

AbstractHandlerMethodMapping은 mapping 정보를 비로소  이용할 수 있게 된다. 

그 후 matches를 우선순위에 맞게 정렬하고,

request와 일치하는 0번째 match를 bestMatch에 담아 

bestMatch의 handlerMethod를 조회해 최종적으로 적합한 handlerMethod를 찾게 된다. 

 

이렇게 request와 handler objects를 매핑하는 Handler Mapping의 동작 원리를 살펴보았다. 

 

이제 mapping strategy를 직접 커스터마이징 하기 위하여 interface를 구현하는 것을 다음 포스팅에서 진행해보겠다.