[@MVC 구현하기] Spring MVC의 HandlerMapping 등록 원리 파헤치기

2025. 9. 22. 09:52·개발/Java & Spring
이 글은 우아한테크코스의 "@MVC 구현하기"미션 중 생긴 의문점을 탐구한 과정을 담은 글입니다.

📌 들어가며

우아한테크코스 레벨4의 두 번째 미션은 “@MVC 구현하기”다.

Spring MVC의 일부 기능을 직접 구현해보면서 MVC 구조를 더 깊게 이해할 수 있는 미션이다.

첫 번째 미션이었던 "Tomcat 구현하기"에 이어, 이번에는 webmvc 프레임워크의 핵심 동작 원리를 간접적으로 경험해볼 수 있었다.

 

미션을 진행하면서 흥미로운 문제에 부딪혔다.

내가 구현한 MVC 프레임워크는, 프론트 컨트롤러 역할의 DispatcherServlet이 존재한다. DispatcherServlet은 2가지 종류의 HandlerMapping, HandlerAdapter를 요청에 따라 적절히 사용해 알맞은 응답을 반환한다.

이때 내가 구현한 DispatcherServlet은 새로운 Handler가 추가될 때마다 코드를 수정해야 하는 구조를 하고 있었다. 이 부분에서 문제 의식이 시작되었다. 그리고 과연 실제 Spring MVC는 이런 문제를 어떻게 해결할지에 대한 궁금증으로 이어졌다.

 

이 글에서는 "변화에 유연하지 못한 구조"라는 문제 의식에서 시작해서, Spring MVC의 내부 코드를 탐구하며 HandlerMapping과 HandlerAdapter가 어떻게 등록되고 관리되는지 알아본 과정을 다룬다.

  • 대상 독자: Tomcat, Servlet, ServletContainer의 기본 개념을 알고 있고, Spring Boot MVC의 작동 방식에 관심이 있는 개발자
  • 핵심 질문: 새로운 Handler가 추가될 때 기존 코드의 수정 없이 처리할 수 있을까?

📌 미션에서 구현한 MVC 구조

문제 상황을 다루기 전에, 먼저 미션에서 구현한 MVC 구조를 살펴보자. 구조에 대한 이해가 있어야 이후 등장할 문제점과 해결 방안을 제대로 이해할 수 있다.

 

톰캣의 서블릿 컨테이너에는 DispatcherServlet이 등록되어 있어서 모든 HTTP 요청이 DispatcherServlet의 service 메서드로 도착한다. DispatcherServlet 부터 top-down으로 하나씩 내려가면서 살펴보자.

모든 코드는 세부 구현 및 예외처리, 로깅 등의 코드는 생략하고 주요 구조만 남겨서 작성했다.

📍 DispatcherServlet 

내가 구현한 코드

public class DispatcherServlet extends HttpServlet {

    private final HandlerMappingRegistry handlermappingRegistry;
    private final HandlerAdapterRegistry handlerAdapterRegistry;

    @Override
    protected void service(
            final HttpServletRequest request,
            final HttpServletResponse response
    ) {
		// 1. Handler 조회
        final Object handler = handlermappingRegistry.getHandler(request);
        
        // 2. HandlerAdapter 조회
        final HandlerAdapter handlerAdapter = handlerAdapterRegistry.getHandlerAdapter(handler);
        
        // 3. HandlerAdapter 실행 -> ModelAndView 반환
        final ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
        
        // 4. View 호출
        render(modelAndView, request, response);
    }
}

 

이 4단계 흐름은 Spring MVC의 핵심이다:

  1. Handler 조회: 요청 URL에 맞는 컨트롤러나 메서드를 찾는다
  2. HandlerAdapter 조회: 찾은 Handler를 실행할 수 있는 어댑터를 찾는다
  3. Handler 실행: 어댑터를 통해 실제 비즈니스 로직을 실행하고 결과를 받는다
  4. View 렌더링: 결과를 바탕으로 화면을 생성해서 응답한다

📍 xxxRegistry

실제 Spring MVC의 DispatcherServlet에는 존재하지 않지만, 미션의 가이드라인에 따라 도입했다. DispatcherServlet이 가지고 있는 List<HandlerMapping과 List<HandlerAdapter> 를 일급 컬렉션으로 포장해서 HandlerMappingRegistry와 HandlerAdapterRegistry로 사용하고 있다. 기존 구조에서 객체지향의 책임 분리를 고려해서 등장한 구조인 것 같다.

public class HandlerMappingRegistry {

    private final List<HandlerMapping> handlerMappings;

    public Object getHandler(final HttpServletRequest request) throws ServletException {
	    ...
    }

    private void addHandlerMapping(final HandlerMapping handlerMapping) {
	    ...
    }
}
public class HandlerAdapterRegistry {

    private final List<HandlerAdapter> handlerAdapters;

    public HandlerAdapter getHandlerAdapter(final Object handler) throws ServletException {
			...
    }

    private void addHandlerAdapter(final HandlerAdapter handlerAdapter) {
			...
    }
}

📍 HandlerMapping

미션에서 사용한 코드

public interface HandlerMapping {

    Object getHandler(HttpServletRequest request);
}

요청에 해당하는 Handler 객체를 찾아서 반환한다. 미션 코드에서는 Map을 이용해서 요청 경로와 Controller 클래스를 매핑하는 ManualHandlerMapping 과 어노테이션을 기반으로 리플렉션을 사용해서 컨트롤러를 찾는 AnnotationHandlerMapping 두 가지 구현체를 사용했다.

Spring MVC의 HandlerMapping

package org.springframework.web.servlet;

public interface HandlerMapping {

	@Nullable
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

Spring MVC 또한 거의 비슷한 HandlerMapping 인터페이스를 가지고 있다. DispatcherServlet이 등록된 HandlerMapping들을 정해진 우선순위에 따라 순서대로 확인하여 핸들러를 찾는다. Spring MVC의 주요 구현체는 다음과 같다.

  • RequestMappingHandlerMapping: @RequestMapping 어노테이션 기반 매핑 (우선순위 0)
  • BeanNameUrlHandlerMapping: URL과 동일한 이름의 빈을 핸들러로 매핑
  • SimpleUrlHandlerMapping: 정적 리소스 처리 등에 사용
  • WelcomePageHandlerMapping: 루트 URL 접근 시 웰컴 페이지 매핑

📍HandlerAdapter

인터페이스

HandlerAdapter는 서로 다른 타입의 핸들러들을 통일된 방식으로 실행하기 위한 어댑터 패턴의 구현체다. 각 핸들러 타입마다 전용 어댑터가 필요하다. 미션의 어뎁터는 Spring MVC의 핸들러어뎁터를 참고해서 동일하게 작성했다.

package org.springframework.web.servlet;

public interface HandlerAdapter {

    boolean supports(final Object handler);

    ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception;
}

구현체

Spring MVC 프레임워크의 경우 각 Handler 마다 HandlerAdapter가 구현되어 있는 것을 확인할 수 있다.

미션 코드 또한 다루고 있는 두 종류의 핸들러 각각의 Adapter가 구현되어 있다.

public class ManualHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(final Object handler) {
        return handler instanceof Controller;
    }

    @Override
    public ModelAndView handle(
            final HttpServletRequest request,
            final HttpServletResponse response,
            final Object handler
    ) throws Exception {
        var viewName = ((Controller) handler).execute(request, response);
        return new ModelAndView(new JspView(viewName));
    }
}
public class AnnotationHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(final Object handler) {
        return handler instanceof HandlerExecution;
    }

    @Override
    public ModelAndView handle(
            final HttpServletRequest request,
            final HttpServletResponse response,
            final Object handler
    ) throws Exception {
        return ((HandlerExecution) handler).handle(request, response);
    }
}

📌 문제 상황

이 글을 쓰게된 문제 상황은 2단계 PR에 달린 아래 리뷰로부터 시작됐다. (thanks to 백터)

https://github.com/woowacourse/java-mvc/pull/986#discussion_r2365261345

📍 변경에 유연하지 못한 구조

미션 코드에서 다음과 같은 구현체들을 사용한다.

  • HandlerMapping 인터페이스의 구현체 → HandlerMappingRegistry 에서 관리
    • ManualHandlerMapping
    • AnnotationHandlerMapping
  • HandlerAdapter 인터페이스의 구현체 → HandlerAdapterRegistry 에서 관리
    • ManualHandlerAdapter
    • AnnotationHandlerAdapter

이 구현체들은 각 Registry의 생성자에서 생성하고 있다. 이 구조의 문제점은 위의 리뷰에서 언급된 것 처럼, 새로운 종류의 핸들러가 추가된다면 HandlerMappingRegistry, HandlerAdapterRegistry를 수정해야 한다.

// 문제가 되는 코드 구조
public class HandlerAdapterRegistry {
    private final List<HandlerAdapter> handlerAdapters;

    // 🚨 문제: 생성자에서 구체 클래스들을 하드코딩
    public HandlerAdapterRegistry() {
        this.handlerAdapters = new ArrayList<>();
        addHandlerAdapter(new ManualHandlerAdapter());        // 기존
        addHandlerAdapter(new AnnotationHandlerAdapter());    // 기존
        // addHandlerAdapter(new NewHandlerAdapter());        // ⚠️ 새 Handler 추가시 여기를 수정해야 함!
    }
    // ... 나머지 코드
}

📍문제 해결을 위한 가설들

어떻게 하면 코드 수정 없이 새로운 Handler를 추가할 수 있을까? 몇 가지 해결 방안이 떠올랐다:

  1. Reflection을 통한 자동 스캔: 특정 패키지를 스캔해서 HandlerMapping, HandlerAdapter 구현체를 자동으로 찾아 등록
  2. 설정 파일 기반 등록: XML이나 Properties 파일에서 사용할 구현체들을 명시

여기까지 생각한 다음에 실제 Spring 의 구현 방식이 궁금해졌다. 좋은 해결 방법이 있으면 해당 방식을 모방해보기 위해서 탐색을 하기 시작했다. 이제 Spring MVC의 내부 구현을 직접 탐구해보자.


📌 Spring MVC의 해결 방식 탐구

📍 DispatcherServlet의 초기화 과정

먼저 DispatcherServlet 에서 List<HandlerMapping> 과 List<HandlerAdapter> 를 어디서 초기화하는지 찾아봤다.

DispatcherServlet의 initStrategies 메서드에서 초기화되고 있다.

protected void initStrategies(ApplicationContext context) {
	initMultipartResolver(context);
	initLocaleResolver(context);
	initThemeResolver(context);
	initHandlerMappings(context); // 여기!
	initHandlerAdapters(context); // 여기!
	initHandlerExceptionResolvers(context);
	initRequestToViewNameTranslator(context);
	initViewResolvers(context);
	initFlashMapManager(context);
}

📍 Bean 컨테이너를 통한 구성 요소 관리

initHandlerMappings와 initHandlerAdapters는 구현이 거의 똑같아서 initHandlerAdapters 메서드만 확인해보자. 이번에도 코드 중 핵심 부분만 골라서 작성했다.

private void initHandlerAdapters(ApplicationContext context) {
    // ApplicationContext에서 HandlerAdapter 타입의 모든 빈을 찾아온다!
	Map<String, HandlerAdapter> matchingBeans =
			BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);

	this.handlerAdapters = new ArrayList<>(matchingBeans.values());
    // 우선순위에 따라 정렬
	AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}

 

오호 여기서 많은 힌트를 얻을 수 있었다. BeanFactoryUtils 클래스가 등장했다. 그리고 HandlerAdapter.class 타입을 beansOfTypeIncludingAncestor 메서드의 인자로 넘겨주고 있다.

Spring MVC는 스프링 빈 컨테이너를 활용해서 HandlerAdapter 구현체들을 동적으로 찾아 등록한다!

📍 WebMvcConfigurationSupport의 역할

그렇다면 HandlerAdapter 구현체들은 어디서 빈으로 등록될까? 답은 WebMvcConfigurationSupport 클래스에 있었다.

  • HandlerMapping
    • RouterFunctionMapping
    • RequestMappingHandlerMapping
    • HandlerMapping
    • BeanNameUrlHandlerMapping
  • HandlerAdapter
    • RequestMappingHandlerAdapter
    • HttpRequestHandlerAdapter
    • SimpleControllerHandlerAdapter
    • HandlerFunctionAdapter
  • HandlerExceptionResolverComposite
    • ExceptionHandlerExceptionResolver
    • ResponseStatusExceptionResolver
    • DefaultHandlerExceptionResolver
  • AntPathMatcher
  • UrlPathHelper
  • PathPatternParser
  • …

위에 있는 Spring MVC의 모든 핵심 구성 요소들이 이 클래스에서 @Bean으로 등록된다:

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {

	@Bean
	public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
			@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
			@Qualifier("mvcConversionService") FormattingConversionService conversionService,
			@Qualifier("mvcValidator") Validator validator) {

		RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
		adapter.setContentNegotiationManager(contentNegotiationManager);
		
		...

		return adapter;
	}
	
	@Bean
	public HandlerFunctionAdapter handlerFunctionAdapter() {
		HandlerFunctionAdapter adapter = new HandlerFunctionAdapter();

		AsyncSupportConfigurer configurer = getAsyncSupportConfigurer();
		if (configurer.getTimeout() != null) {
			adapter.setAsyncRequestTimeout(configurer.getTimeout());
		}
		return adapter;
	}

	@Bean
	public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
		return new HttpRequestHandlerAdapter();
	}

	@Bean
	public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
		return new SimpleControllerHandlerAdapter();
	}	
}

📌 결론 

📍 예상 밖의 탐구 결과

"새로운 Handler 추가 시 코드 수정 없이 처리할 수 있을까?"라는 단순한 질문으로 시작했지만, Spring MVC의 내부를 파헤쳐보니 예상과 다른 답을 발견했다.

 

Spring MVC도 새로운 Handler가 추가될 때는 코드 변경이 불가피하다는 것이었다. 내가 기대했던 Reflection 기반 자동 스캔이나 설정 파일을 통한 마법 같은 해결책은 없었다. 대신 Spring은 빈 컨테이너라는 중앙화된 관리 시스템을 통해 이 문제를 해결하고 있었다. WebMvcConfigurationSupport에서 모든 구성 요소를 체계적으로 관리하는 방식 말이다.

 

생각해보니 이게 더 현실적인 접근법이었다. 완전한 자동화보다는 예측 가능하고 명시적인 관리가 실제 운영에서는 더 안전하다는 걸 깨달았다.

📍 미션에서의 아쉬운 점

솔직히 말하면 이 모든 탐구가 완벽한 해결책으로 이어지지는 못했다. Spring의 빈 컨테이너 방식을 미션에 적용하려면 기존 코드를 거의 다 뜯어고쳐야 했고, 그건 미션의 범위를 한참 벗어나는 작업이었다.

 

결국 리뷰어에게 "현재 구조의 한계를 인식하고 있지만, 시간상 개선하지 못했다"고 정직하게 설명하며 마무리했다. 처음엔 좀 찜찜했지만, 지금 생각해보니 문제를 정확히 파악하고 트레이드오프를 이해한 것만으로도 의미 있는 학습이었던 것 같다.

📍 그럼에도 불구하고

이 과정에서 얻은 것들이 정말 많았다. 무엇보다 DispatcherServlet, HandlerMapping, HandlerAdapter 같은 용어들이 더 이상 추상적인 개념이 아니라 실제로 살아 움직이는 코드로 느껴진다는 게 가장 큰 변화다.

 

Spring MVC의 소스 코드를 직접 들여다보면서, 처음엔 복잡해 보이던 구조가 사실은 매우 논리적이고 체계적으로 설계되어 있다는 걸 알 수 있었다. 예측하고, 찾고, 파악하는 과정이 자체도 참 재밌었다.

 

그리고 이런 탐구를 통해 오픈소스 코드를 읽는 나만의 방법도 조금씩 생기고 있는 것 같다. 처음엔 어디서부터 봐야 할지 막막했는데, 이제는 인터페이스부터 시작해서 구현체를 찾아가는 패턴이 어느 정도 익숙해졌다.

 

우테코 레벨2에서 스프링을 배우면서 의문이 생기는 것들에 대해 코치님들이 레벨4에서 해결할 수 있다고 대답하곤 했는데, 진짜로 그게 딱 맞아 떨어져서 신기하다ㅎㅎ 앞으로의 미션들도 기대가 된다!

 


미션 구현 코드

 

GitHub - yesjuhee/java-mvc

Contribute to yesjuhee/java-mvc development by creating an account on GitHub.

github.com

 

미션 제출 PR/리뷰

 

[2단계 - 점진적인 리팩터링] 노랑 미션 제출합니다 by yesjuhee · Pull Request #986 · woowacourse/java-mvc

기능 요구사항 구현 & 테스트 Controller 인터페이스 기반의 레거시 MVC와, 1단계에서 추가된 @controller 어노테이션 기반의 MVC가 함께 작동하는 구조를 구현했습니다. 해당 기능을 실제로 테스트 해보

github.com

 

'개발 > Java & Spring' 카테고리의 다른 글

DispatcherServlet의 service() 메서드는 어디에?  (0) 2025.09.29
Tomcat 구현하기 삽질기 (feat.acceptCount)  (2) 2025.09.15
Fixture와 Builder 패턴으로 테스트 코드 가독성 높이기  (4) 2025.07.25
'개발/Java & Spring' 카테고리의 다른 글
  • DispatcherServlet의 service() 메서드는 어디에?
  • Tomcat 구현하기 삽질기 (feat.acceptCount)
  • Fixture와 Builder 패턴으로 테스트 코드 가독성 높이기
yesjuhee
yesjuhee
Dopamine Driven Developer
  • yesjuhee
    나랑 노랑
    yesjuhee
  • 전체
    오늘
    어제
    • 분류 전체보기 (29)
      • 개발 (11)
        • DevOps (2)
        • Java & Spring (4)
        • AI (1)
        • DB (1)
        • 기타 (3)
      • 후기 or 회고 (15)
        • 우아한테크코스 (11)
        • 기타 (4)
      • 독서 (2)
      • 기타 (1)
      • 초록 스터디 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Ai
    모아온
    초록 밋업
    초록 스터디
    레벨2
    DispatcherServlet
    레벨4
    coderabbit
    SCG
    QueryDSL
    독서
    소프티어 부트캠프
    claude code
    spring
    후기
    우아콘
    레벨3
    우테코
    바킹독
    mysql
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
yesjuhee
[@MVC 구현하기] Spring MVC의 HandlerMapping 등록 원리 파헤치기
상단으로

티스토리툴바