Spring MVC의 위치와 동작 흐름
Spring MVC는 Spring 프레임워크의 웹 계층 모듈로, MVC 패턴을 기반으로 하는 요청-응답 처리를 담당합니다.
요청이 들어오면 다음과 같은 순서로 처리됩니다:
- 모든 요청은 DispatcherServlet(프론트 컨트롤러)이 가장 먼저 수신합니다.
- DispatcherServlet이 HandlerMapping을 통해 요청 URL에 맞는 Handler(컨트롤러)를 조회합니다. (XML 설정 또는 애노테이션 기반)
- HandlerMapping이 찾은 Handler 정보를 DispatcherServlet에 반환합니다.
- DispatcherServlet은 해당 Handler를 실행할 수 있는 HandlerAdapter를 호출합니다.
- HandlerAdapter가 실제 Handler(컨트롤러)를 실행합니다.
- Handler 실행 결과로 ModelAndView 객체를 반환합니다.
- HandlerAdapter는 이 ModelAndView를 DispatcherServlet에 전달합니다.
- DispatcherServlet이 ViewResolver에 논리 뷰 이름을 전달하여 실제 뷰(JSP, FreeMarker 등)로 변환을 요청합니다.
- ViewResolver가 실제 View 객체를 반환합니다.
- 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