[Spring] FrontController V4 - 개발자 친화적 리팩토링
[Spring] FrontController 도입하기 V3 - 서블릿 종속성 제거[Spring] FrontController 도입하기 V2 - 포워딩 중복 제거https://bdisappointed.tistory.com/154 [Spring] FrontController 도입하기 V1[Spring] 서블릿과 JSP로 MVC 흉내내
bdisappointed.tistory.com
이전에 실용적이고 개발자 친화적인 MVC 구조를 구현 하였다.
이번에는 다양한 인터페이스를 처리할 수 있도록 컨트롤러를 리팩토링 해보자
Adapter 패턴 적용
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
위와 같이 서로 다른 인터페이스를 처리하려면 어떤식으로 구현 해야할까?
일본에 놀러가서 220v 콘센트에 110v 어댑터를 끼우고 사용하는 예시를 생각하면 편하다.
이와 같이 우리도 어댑터 패턴을 적용하여 다양한 인터페이스를 처리하도록 구현해보자.

컨트롤러의 이름을 더 넓은 의미의 핸들러로 수정했다. 그리고 이제 핸들러 어댑터를 통해 해당 핸들러(컨트롤러)를 가져오는 역할을 한다.
HandlerAdapter 인터페이스 구현
package hello.servlet.web.frontcontroller.v5;
import hello.servlet.web.frontcontroller.ModelView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
supports 변수는 이 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드이다.
우리는 이제 이 어댑터를 통해 실제 핸들러(컨트롤러)를 호출하고, 그 컨트롤러의 결과로 ModelView를 반환할 것이다.
이전에는 프론트 컨트롤러를 통해 컨트롤러를 실제 호출 했지만, 이제는 프론트 컨트롤러 -> 핸들러 어댑터 -> 핸들러(컨트롤러) 로 호출 관계를 변경할 것 이다!
V3 HandlerAdapter
[Spring] FrontController 도입하기 V3 - 서블릿 종속성 제거
[Spring] FrontController 도입하기 V2 - 포워딩 중복 제거https://bdisappointed.tistory.com/154 [Spring] FrontController 도입하기 V1[Spring] 서블릿과 JSP로 MVC 흉내내기 , 그리고 한계점서블릿과 JSP를 통해 MVC를 구현 해
bdisappointed.tistory.com
해당 포스팅에서 만들었던 v3 컨트롤러를 판별하는 핸들러 어댑터를 만들자.
package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
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; // 1. 컨트롤러를 타입에 맞게 캐스팅
Map<String, String> paramMap = createParamMap(request); // 2. request의 파라미터이름과 값을 파라 맵으로 따로 관리
ModelView mv = controller.process(paramMap);
return mv;
}
private static 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;
}
}
supports는 가져온 핸들러가 v3의 구현체인지 검사한다.
이후 handle()에는 해당 컨트롤러를 해당 타입에 맞게 캐스팅 해준다(기존에 Object 타입으로 받았기 때문).
그 다음엔 우리가 이전에 했던 것 처럼 request 에서 모든 파라미터를 가져와 paramMap을 만들어준다.
그 다음 해당 컨트롤러에 paramMap을 넘긴 process() 메서드를 실행한다.
이제 이해하기 쉽게 회원을 저장하는 코드와 함께 로직을 이해해 보자.
package hello.servlet.web.frontcontroller.v3.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import java.util.Map;
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); // 해당 모델 뷰 안에 Map 모델 키값 입력
return mv;
}
}
paramMap에는 모든 파라미터 정보가 저장이 되어있기 때문에, 이를 기반으로 Member 객체를 만든다.
해당 Member 객체를 저장소에 저장하고, save-result라는 논리적 뷰 이름을 가진 ModelView 객체를 생성하고, 해당 객체에 member 데이터를 담아 ModelView 객체를 반환한다.
이후 어댑터에서도 다시 ModelView 객체를 반환한다.
이어서 v4에 맞는 어댑터도 구현해보자.
V4 HandlerAdapter
[Spring] FrontController V4 - 개발자 친화적 리팩토링
[Spring] FrontController 도입하기 V3 - 서블릿 종속성 제거[Spring] FrontController 도입하기 V2 - 포워딩 중복 제거https://bdisappointed.tistory.com/154 [Spring] FrontController 도입하기 V1[Spring] 서블릿과 JSP로 MVC 흉내내
bdisappointed.tistory.com
package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
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) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private static 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;
}
}
v4도 v3와 로직은 같다.
이번에는 v4의 회원 저장 로직을 보며 분석해보자.
package hello.servlet.web.frontcontroller.v4.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.Map;
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";
}
}
v3와 다른 부분이라면 v3는 ModelView를 반환 했던 반면에 v4는 String (논리적 뷰 이름) 만 반환한다.
또한 v4는 paramMap도 받고, model도 받아야 한다.
v4 어댑터 클래스를 보면 v3의 초반 로직 (컨트롤러 타입 검증 + 캐스팅) 과 똑같다.
이후에 v4 타입 핸들러(컨트롤러) 임이 검증 되면 해당 핸들러에 paramMap과 model을 그때 만들어서 넣어준다. 이전에는 모델을 만들어 전달하는 기능도 프론트 컨트롤러가 담당했는데, 이제는 핸들러 어댑터가 해당 기능을 수행해준다.
이후 생성된 paramMap과 model객체를 v4 타입 핸들러로 넘겨 비즈니스 로직을 수행한뒤, 그 결과를 모델에 담고 논리적 뷰 이름만 반환해준다.
중간 점검
여태 우리는 v3에 맞는 어댑터와 v4에 맞는 어댑터를 만들었다.
이제 이 둘을 모두 처리할 수 있는 프론트 컨트롤러가 필요하다.
프론트 컨트롤러
package hello.servlet.web.frontcontroller.v5;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
@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() {
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());
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(SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName(); // members
MyView view = viewResolver(viewName); //WEB-INF/views/"members.jsp를 viewPath로 가진 MyView 생성
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 static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
동작 과정
v5으로 시작하는 모든 URL는 해당 서블릿이 호출된다.
서블릿이 서블릿 컨테이너에 등록 되며 생성자가 실행되고, 생성자의 내용인 initHandlerMappingMap() 메서드와 initHandlerAdapters() 메서드가 실행이된다. (각각 여태 만든 컨트롤러와 핸들러를 등록하는 작업)
이후 service() 메서드가 실행 된다. 이때 이 코드의 흐름 분기는 2개로 나뉘어진다 -> v3 컨트롤러와 v4 컨트롤러
1. getHandler() 메서드가 실행 된다. -> 해당 URL에 해당하는 핸들러(컨트롤러)를 뱉어준다.
2. 해당하는 컨트롤러가 없은 404 에러를 띄운다
3. 그래서 해당 핸들러를 getHandlerAdapter() 메서드의 파라미터로 넘겨 해당 핸들러의 맞는 어댑터를 가져온다
3-1 : handlerAdapters에는 v3어댑터와 v4 어댑터가 들어 있는데, 루프를 돌며 supports를 검사하여 알맞는 핸들러 어댑터를 가져온다.
4. 가져온 어댑터를 통해 handle 메서드를 실행한다. (v3 기준 캐스팅, paramMap 생성, 컨트롤러 비즈니스 로직 수행 , modelView 반환 / v4 기준 캐스팅, paramMap 생성, model 객체 생성, 컨트롤러 비즈니스 로직 수행, 반환 받은 논리적 뷰 이름 기반 modelView 반환)
5. 반환 받은 modelView 객체에서 논리적 뷰 이름 추출 -> 리졸버로 절대 뷰 경로로 변환 -> reder 메서드로 JSP 코드로 포워딩
마지막으로 model, request, response 파라미터를 통해 render 되는 과정을 정리하면, model 에 담겨 있는 데이터를 request의 attribute로 옮겨 담는다.
Dispatcher은 request 기반으로만 생성할 수 있기 때문에 데이터를 옮겨 담았다.
이후 request.getReqeustDispatcher을 통해 포워딩할 dispatcher 서블릿을 반환 받고 해당 경로로 포워딩 한다.
결론
우리는 이제 다양한 인터페이스의 구현체를 처리할 수 있는 MVC 패턴을 진짜 처음부터 끝까지 구현해보았다.
이제 만약 새로운 타입을 추가하게 된다면 다음과 같이 3가지만 추가하고, 프론트 컨트롤러에 추가하기만 하면 될 것이다 !
- ControllerVN
- ControllerVN 의 구현체들
- ContorllerVNHandlerAdapter
+ 프론트 컨트롤러에 구현체와 어댑터만 추가
진짜 길고 힘들었지만, MVC 구조를 완벽하게 이해했다. MVC 패턴이야말로 다형성의 극치라고 생각한다ㅋㅋ
조금만 쉬고, 지금 우리가 구현한 MVC 패턴과 거의 유사한 스프링이 제공하는 MVC 패턴에 대해서도 즐겁게 알아보자!
그럼 20000!
'Spring > MVC' 카테고리의 다른 글
| [Spring] Spring MVC - 스프링이 제공하는 컨트롤러 (0) | 2025.06.23 |
|---|---|
| [Spring] 내가 만든 MVC 와 Spring MVC 차이 (1) | 2025.06.23 |
| [Spring] FrontController V4 - 개발자 친화적 리팩토링 (0) | 2025.06.23 |
| [Spring] FrontController 도입하기 V3 - 서블릿 종속성 제거 (0) | 2025.06.23 |
| [Spring] FrontController 도입하기 V2 - 포워딩 중복 제거 (0) | 2025.06.23 |