1. 프론트 컨트롤러(Front Controller) 패턴 소개
과거의 방식은 모든 서블릿이 각자의 로직을 수행하며 공통 처리를 중복으로 수행해야 했다. 프론트 컨트롤러 패턴은 '입구를 하나로' 모아 공통 로직을 통합 관리하는 것이 핵심이다.


- 특징: 서블릿 하나로 모든 클라이언트 요청을 받고, 요청에 맞는 컨트롤러를 찾아 호출한다.
- 스프링과의 관계: 스프링 웹 MVC의 DispatcherServlet이 바로 이 프론트 컨트롤러 패턴으로 구현되어 있다.
2. v1: 프론트 컨트롤러 도입 (기본 구조 구축)
기존 서블릿 코드를 최대한 유지하면서 프론트 컨트롤러만 도입한 단계이다.
ControllerV1 인터페이스
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
MemberFormControllerV1 (회원 등록)
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
MemberSaveControllerV1 (회원 저장)
public class MemberSaveControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
FrontControllerServletV1
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
v1의 문제점: 각 컨트롤러마다 RequestDispatcher.forward() 로직이 중복됨
3. v2: View 분리 (중복 제거)
모든 컨트롤러에서 반복되는 RequestDispatcher.forward() 로직을 별도의 객체로 분리한다.
MyView 객체 - 뷰 렌더링 로직 캡슐
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
ControllerV2 인터페이스
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
컨트롤러는 이제 직접 화면을 이동시키지 않고 MyView 객체를 반환하기만 한다.
MemberFormControllerV2
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
FrontControllerServletV2
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
private Map<String, ControllerV2> controllerMap = new HashMap<>();
public FrontControllerServletV2() {
controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView view = controller.process(request, response);
view.render(request, response);
}
}
v2의 개선점: 뷰 렌더링 로직이 MyView로 추상화되어 중복 제거
4. v3: Model 추가 (서블릿 종속성 제거)
컨트롤러가 서블릿 기술(Request, Response)을 몰라도 동작할 수 있도록 설계한다. 이를 통해 테스트 코드를 작성하기가 매우 쉬워진다.
ModelView 객체
public class ModelView {
private String viewName; // 논리적 뷰 이름 (예: "new-form", "save-result")
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
// Getter, Setter
public String getViewName() { return viewName; }
public void setViewName(String viewName) { this.viewName = viewName; }
public Map<String, Object> getModel() { return model; }
public void setModel(Map<String, Object> model) { this.model = model; }
}
ControllerV3 인터페이스
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
MemberSaveControllerV3
public class MemberSaveControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
FrontControllerServletV3
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1. 요청 파라미터를 Map으로 변환 (paramMap)
Map<String, String> paramMap = createParamMap(request);
// 2. 컨트롤러 호출 (서블릿 종속성 없음)
ModelView mv = controller.process(paramMap);
// 3. ViewResolver 호출: 논리 이름을 물리 경로로 변환
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
// 4. 렌더링 (모델 데이터 포함)
view.render(mv.getModel(), request, response);
}
MyView 업데이트 (모델 지원 추가)
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
public void render(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
private void modelToRequestAttribute(Map<String, Object> model,
HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
v3의 개선점:
1) 서블릿 종속성 완전 제거 (테스트 용이)
2) 논리적 뷰 이름 사용 (물리적 경로 변경 시 프론트 컨트롤러만 수정)
3) Model 객체 도입으로 데이터 전달 명확화
5. v4: 단순하고 실용적인 컨트롤러 (개발 편의성 증대)
v3는 구조적으로 완벽하지만, 개발자가 매번 ModelView 객체를 생성해서 반환해야 하는 번거로움이 있다. 이를 개선하여 뷰의 이름(String)만 반환하도록 변경한다.
ControllerV4 인터페이스
public interface ControllerV4 {
/**
* @param paramMap 요청 파라미터 맵
* @param model 뷰에 전달할 모델 객체
* @return viewName (논리적 뷰 이름)
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
MemberSaveControllerV4
public class MemberSaveControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
model.put("member", member); // 파라미터로 받은 모델에 직접 추가
return "save-result"; // 뷰 이름만 반환
}
}
FrontControllerServletV4
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
private Map<String, ControllerV4> controllerMap = new HashMap<>();
public FrontControllerServletV4() {
controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); // 모델 객체 생성
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
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;
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
v4의 개선점: 개발자 편의성 증대 - ModelView 객체 생성 없이 뷰 이름만 반환
6. v5: 유연한 컨트롤러 (어댑터 패턴 도입)
문제 상황
1) v3 방식 선호 개발자와 v4 방식 선호 개발자가 함께 작업해야 함
2) 서로 다른 인터페이스를 하나의 프레임워크에서 지원해야 함
MyHandlerAdapter 인터페이스
public interface MyHandlerAdapter {
// 해당 핸들러를 지원하는가?
boolean supports(Object handler);
// 어댑터는 실제 컨트롤러를 호출하고, 결과를 ModelView로 반환
ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException;
}
ControllerV3HandlerAdapter
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) {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
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;
}
}
ControllerV4HandlerAdapter
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
// V4는 String을 반환하지만, 어댑터는 ModelView로 변환
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
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;
}
}
FrontControllerServletV5
@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 {
// 1. 핸들러(컨트롤러) 조회
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 2. 핸들러를 처리할 수 있는 어댑터 조회
MyHandlerAdapter adapter = getHandlerAdapter(handler);
// 3. 어댑터 실행
ModelView mv = adapter.handle(request, response, handler);
// 4. 뷰 렌더링
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를 찾을 수 없습니다. handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
v5의 개선점: 어댑터 패턴으로 다양한 컨트롤러 인터페이스 지원
정리: MVC 프레임워크 진화 과정
| 버전 | 주요 개선점 | 핵심 아이디어 |
| v1 | 프론트 컨트롤러 도입 | 요청의 입구를 하나로 통합하고, 공통 처리를 중앙에서 관리 |
| v2 | View 객체 분리 | 뷰 렌더링 로직을 캡슐화하여 중복을 제거 |
| v3 | Model 도입, 서블릿 종속성 제거 | 테스트가 쉬운 구조로 개선하고, 논리적 뷰 이름 사용 |
| v4 | 인터페이스 단순화 | 컨트롤러 구현을 단순화하여 개발자 편의성 증대 |
| v5 | 어댑터 패턴 도입 | 다양한 컨트롤러를 수용할 수 있도록 유연성과 확장성 극대화 |
'Spring > MVC' 카테고리의 다른 글
| [MVC-6] CSR 환경에서 @RequestBody만 쓸까? (feat. 파일 업로드와 @ModelAttribute) (0) | 2026.02.18 |
|---|---|
| [MVC-5] 스프링 MVC 기본 기능 (0) | 2025.12.30 |
| [MVC-4] 스프링 MVC 시작하기: 애노테이션 기반 컨트롤러 (0) | 2025.12.30 |
| [MVC-3] Spring MVC 구조 이해 (0) | 2025.12.30 |
| [MVC-1] 서블릿에서 JSP까지: 자바 백엔드 웹 기술의 발전과 한계 (0) | 2025.12.30 |
