FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
FrontController와 핸들러 어댑터를 직접 구현해 보며 DispatcherServlet이 요청을 처리하는 구조를 이해한다.
시리즈 · Spring4 / 14
- SOLID 원칙과 OCP·DIP를 지키는 방법
- 의존성 주입(DI)과 스프링 컨테이너
- 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
- FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
- @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
- System.out 대신 SLF4J로 로그 남기기
- 세션 직접 만들어 보기와 HttpSession
- 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
- 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
- JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
- JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
- JPA 변경 감지(dirty checking)와 병합(merge)
- 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
- 싱글톤 빈의 동시성 문제와 ThreadLocal
스프링 MVC는 FrontController 패턴을 기반으로 한다. 패턴의 개념과 스프링 MVC의 구조를 먼저 보고, 같은 구조를 직접 구현한 코드로 확인한다.
FrontController 패턴
FrontController가 없으면 컨트롤러마다 공통 로직을 반복해서 작성해야 한다. 예를 들어 뷰로 이동하는 아래 코드가 모든 컨트롤러에 들어간다.
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

FrontController 패턴은 서블릿 하나가 모든 요청을 받아 공통 로직을 처리한 뒤, 요청에 맞는 컨트롤러를 찾아 호출한다. 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.

스프링 MVC 구조

스프링 MVC의 FrontController는 DispatcherServlet이다. HttpServlet을 상속받아 요청을 받고, 요청에 맞는 핸들러와 핸들러 어댑터를 찾아 실행한다.
- 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러를 조회한다.
- 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
- 핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.
- 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.
- ModelAndView 반환: 핸들러 어댑터가 핸들러의 반환 정보를
ModelAndView로 변환해 반환한다. - viewResolver 호출: 뷰 리졸버를 찾아 실행한다.
- View 반환: 뷰 리졸버가 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링을 담당하는 뷰 객체를 반환한다.
- 뷰 렌더링: 뷰를 통해 렌더링한다.
직접 구현해 보기
강의에서는 V1부터 V5까지 프레임워크를 단계적으로 발전시키는데, 여기서는 최종 버전인 V5만 정리한다. V5의 핵심은 어떤 종류의 컨트롤러든 유연하게 다룰 수 있다는 점이다.

- 핸들러 어댑터: FrontController와 컨트롤러 사이에서 다양한 종류의 컨트롤러를 호출할 수 있게 해 준다.
- 핸들러: 컨트롤러보다 넓은 개념이다. 해당 종류를 처리할 어댑터만 있으면 무엇이든 처리할 수 있다.
어댑터 인터페이스
public interface MyHandlerAdapter {
// 어댑터가 해당 핸들러(컨트롤러)를 처리할 수 있는지 판단한다.
boolean supports(Object handler);
// 실제 컨트롤러를 호출하고 그 결과로 ModelView를 반환한다.
// 컨트롤러가 ModelView를 반환하지 못하면 어댑터가 직접 생성해서라도 반환한다.
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
ControllerV3를 지원하는 어댑터
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
// supports를 먼저 거치므로 캐스팅해도 안전하다.
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
return controller.process(paramMap);
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
다른 형태의 컨트롤러가 추가되어도 그에 맞는 어댑터만 구현하면 된다.
FrontController
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
// V3
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
// V4
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter is not found");
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
동작 흐름
- 생성자에서 핸들러 매핑 정보와 어댑터 목록을 등록한다.
getHandler()로 요청 URI에 맞는 핸들러를 찾는다. 없으면 등록되지 않은 URI이므로 404를 반환한다.getHandlerAdapter()로 핸들러를 처리할 수 있는 어댑터를 찾는다.adapter.handle()로 어댑터를 통해 실제 컨트롤러를 호출한다.- 뷰 리졸버로 뷰를 찾고
render()를 호출한다. 모델을 request attribute에 담고dispatcher.forward()로 JSP에 넘긴다.
앞에서 본 스프링 MVC의 동작 순서와 같은 구조다. 실제 스프링 MVC는 더 복잡하게 구현되어 있지만 기본 흐름은 같고, 애노테이션 기반으로 편리하게 사용할 수 있다는 점이 다르다.
참고
- 김영한, 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (인프런)