Spring MVC 아키텍처와 핸들러 매핑 심층 분석

Spring MVC의 위치와 동작 흐름

Spring MVC는 Spring 프레임워크의 웹 계층 모듈로, MVC 패턴을 기반으로 하는 요청-응답 처리를 담당합니다.

요청이 들어오면 다음과 같은 순서로 처리됩니다:

  1. 모든 요청은 DispatcherServlet(프론트 컨트롤러)이 가장 먼저 수신합니다.
  2. DispatcherServlet이 HandlerMapping을 통해 요청 URL에 맞는 Handler(컨트롤러)를 조회합니다. (XML 설정 또는 애노테이션 기반)
  3. HandlerMapping이 찾은 Handler 정보를 DispatcherServlet에 반환합니다.
  4. DispatcherServlet은 해당 Handler를 실행할 수 있는 HandlerAdapter를 호출합니다.
  5. HandlerAdapter가 실제 Handler(컨트롤러)를 실행합니다.
  6. Handler 실행 결과로 ModelAndView 객체를 반환합니다.
  7. HandlerAdapter는 이 ModelAndView를 DispatcherServlet에 전달합니다.
  8. DispatcherServlet이 ViewResolver에 논리 뷰 이름을 전달하여 실제 뷰(JSP, FreeMarker 등)로 변환을 요청합니다.
  9. ViewResolver가 실제 View 객체를 반환합니다.
  10. DispatcherServlet이 View에 모델 데이터를 전달하여 HTML을 생성하고, 최종적으로 클라이언트에게 응답합니다.

핵심 구성 요소:

  • DispatcherServlet: 요청을 중앙에서 수신하고 분배하는 프론트 컨트롤러
  • HandlerMapping: URL을 기반으로 적절한 핸들러를 찾는 역할
  • HandlerAdapter: 다양한 타입의 핸들러(Controller, HttpRequestHandler 등)를 실행할 수 있게 변환
  • Handler: 실제 비즈니스 로직을 수행하는 컨트롤러
  • ViewResolver: 논리 뷰 이름을 물리적인 뷰 파일로 매핑
  • View: HTML, JSON, PDF 등 다양한 형태의 응답을 생성하는 인터페이스

개발 환경 구성

Spring MVC 사용을 위한 핵심 라이브러리:

  • spring-webmvc
  • spring-context
  • spring-web
  • javax.servlet-api (서블릿 컨테이너)

1. DispatcherServlet 설정 (web.xml)

<!-- Spring MVC 프론트 컨트롤러 등록 -->
<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 
        Spring MVC 설정 파일 위치 지정.
        생략 시 /WEB-INF/[servlet-name]-servlet.xml (예: springmvc-servlet.xml) 기본 로드
    -->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <!-- 
        URL 패턴 옵션:
        1. *.action : .action으로 끝나는 요청만 처리 (명시적)
        2. / : 모든 요청을 처리 (RESTful 스타일 지원)
        3. /* : 비권장. JSP 요청도 가로채서 오류 발생 가능
    -->
    <url-pattern>*.action</url-pattern>
</servlet-mapping>

2. Spring MVC 설정 파일 (springmvc.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 name="/queryItems.action" class="com.example.controller.ItemController1" />

    <!-- 핸들러 매핑: 빈 이름(URL) 기반 -->
    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />

    <!-- 핸들러 어댑터: Controller 인터페이스 구현체 실행 -->
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />

    <!-- 뷰 리졸버: JSP 뷰 처리 (JSTL 필요) -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" />
</beans>

3. 핸들러 구현 (Controller 인터페이스)

public class ItemController1 implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse res) throws Exception {
        // 데이터 준비 (실제로는 서비스 계층 호출)
        List<Item> items = new ArrayList<>();
        
        Item item1 = new Item();
        item1.setName("ThinkPad 노트북");
        item1.setPrice(6000f);
        item1.setDetail("Lenovo ThinkPad T430");
        
        Item item2 = new Item();
        item2.setName("아이폰");
        item2.setPrice(5000f);
        item2.setDetail("Apple iPhone 6");
        
        items.add(item1);
        items.add(item2);
        
        // ModelAndView에 데이터와 뷰 지정
        ModelAndView mv = new ModelAndView();
        mv.addObject("itemList", items);
        mv.setViewName("/WEB-INF/jsp/itemList.jsp");
        return mv;
    }
}

4. 뷰 템플릿 (itemList.jsp)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<html>
<head><title>상품 목록</title></head>
<body>
<table border="1" width="100%">
    <tr>
        <td>상품명</td>
        <td>가격</td>
        <td>생성일</td>
        <td>설명</td>
        <td>관리</td>
    </tr>
    <c:forEach items="${itemList}" var="item">
    <tr>
        <td>${item.name}</td>
        <td>${item.price}</td>
        <td><fmt:formatDate value="${item.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
        <td>${item.detail}</td>
        <td><a href="${pageContext.request.contextPath}/item/editItem.action?id=${item.id}">수정</a></td>
    </tr>
    </c:forEach>
</table>
</body>
</html>

테스트 URL: http://localhost:8080/springMVC/queryItems.action

고급 설정: 다양한 매핑 및 어댑터

SimpleUrlHandlerMapping을 이용한 URL 매핑

동일한 핸들러에 여러 URL을 매핑할 수 있습니다. 여러 매핑 전략을 동시에 사용할 수 있으며, DispatcherServlet이 적절한 매핑기를 선택합니다.

<!-- 핸들러 빈 (id로 참조) -->
<bean id="itemController1" name="/queryItems.action" class="com.example.controller.ItemController1" />

<!-- BeanNameUrlHandlerMapping (빈 이름 기반) -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />

<!-- SimpleUrlHandlerMapping (명시적 URL 매핑) -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <props>
            <!-- itemController1 빈을 /queryItems1.action과 /queryItems2.action에 매핑 -->
            <prop key="/queryItems1.action">itemController1</prop>
            <prop key="/queryItems2.action">itemController1</prop>
        </props>
    </property>
</bean>

이제 /queryItems.action, /queryItems1.action, /queryItems2.action 모두 동일한 핸들러로 연결됩니다.

HttpRequestHandlerAdapter를 사용한 또 다른 핸들러 방식

Controller 인터페이스 외에도 HttpRequestHandler 인터페이스를 구현하여 핸들러를 만들 수 있습니다. 이 방식은 Servlet API에 더 직접적으로 접근할 수 있어 JSON 응답 등에 유리합니다.

<!-- 핸들러 등록 -->
<bean id="itemController2" name="/queryItems3.action" class="com.example.controller.ItemController2" />

<!-- 핸들러 매핑 -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />

<!-- HttpRequestHandler를 지원하는 어댑터 -->
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" />

<!-- 뷰 리졸버 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" />

핸들러 코드:

public class ItemController2 implements HttpRequestHandler {

    @Override
    public void handleRequest(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        
        // 데이터 준비
        List<Item> items = new ArrayList<>();
        items.add(new Item("맥북 프로", 3500f, "Apple M3 Pro"));
        items.add(new Item("갤럭시 S24", 1500f, "삼성 플래그십 폰"));
        
        // 모델 데이터를 request 속성으로 설정
        req.setAttribute("itemList", items);
        
        // JSP로 포워드
        req.getRequestDispatcher("/WEB-INF/jsp/itemList.jsp").forward(req, res);
        
        // JSON 응답 예시 (주석 처리됨)
        /*
        res.setCharacterEncoding("UTF-8");
        res.setContentType("application/json;charset=UTF-8");
        String json = "{\"items\": [...]}";
        res.getWriter().write(json);
        */
    }
}

테스트 URL: http://localhost:8080/springMVC/queryItems3.action

태그: Spring MVC DispatcherServlet HandlerMapping HandlerAdapter SimpleControllerHandlerAdapter

5월 25일 18:20에 게시됨