이전 포스팅에서는 Model개념을 적용하여 request.serAttribute로 수행하던 파라미터 값을 가져오는 것을 Model을 통해 해결함으로 Servlet에 종속성을 제거하였다.
또 뷰 리졸버를 사용해서 각 컨트롤러에서는 View의 논리적 이름만 반환하도록 설계하여 중복 코드를 제거하고 유지보수 측면에서도 효율성을 높여보았다.
MVC패턴과 변천사 - Model과 뷰 리졸버
저번 글에서 MVC패턴이란 무엇이며 왜 사용되는지 쭉 알아보았었고 View를 분리해내고 구현 컨트롤러에서는 View에 주소만 리턴해주고 렌더링은 프론트 컨트롤러에서 처리하도록 설계해봤었다.
lee-dev-log.tistory.com
이번 포스팅에서 공부할 내용은 핸들러 개념을 적용하여 컨트롤러의 버전을 유연하게 변경/확장 가능하도록 설계를 하는 것이다.
지금까지 MVC패턴의 변화과정을 점진적으로 공부하면서 컨트롤러에서 반환했던 타입을 살펴보면
public void process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
위 코드와 같이 처음에는 컨트롤러 자체에서 disapthcher를 통해 직접 View를 렌더링하도록 설계되어있었으며
두번째 버전은 MyView타입을 반환하여 컨트롤러 내부에서 기능에 맞는 View를 viewPath만 설정해 준 뒤 넘기도록하고 렌더링은 프론트 컨트롤러에서 하도록 설계되어있었다.
public MyView process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
가장 간단하게 설계된 버전에서는 String타입을 반환하여 View의 논리 주소만 반환하여 프론트 컨트롤러에서 뷰 리졸버를 통해 실제 사용되는 물리주소로 변경, 렌더링 처리까지 모두 처리하도록 설계되어있었다.
public String process(Map<String, String> pramMap, Map<String, Object> model) {
return "new-form";
}
만약 개발하는 과정에서 String타입으로 반환받아야하는 경우가 생길수도있고, View를 반환받아 개발해야하는 경우가 생길수도 있을 것이다. 이 부분은 실무를 겪어야만 공감될듯하다
하지만 지금까지의 프론트 컨트롤러를 살펴보면 컨트롤러를 조회해서 매핑하는 과정에서 Map의 밸류값으로 ControllerV4와 같이 컨트롤러의 버전을 고정해서 사용하도록 설계되어있음을 아래 코드를 보면 알 수 있다.
private Map<String, ControllerV4> controllerMap = new HashMap<>();
이 부분을 리팩토링하여 여러버전의 컨트롤러를 사용할 수 있도록 유연하게 설계함으로써 객체지향의원칙 OCP를 만족할 수 있도록 설계해보자
Handler와 HandlerAdapter
Handler
핸들러는 컨트롤러와 같은 의미로 사용되지만 컨트롤러보다는 조금 더 넓은 의미로 사용된다.
그 이유로 지금까지의 컨트롤러는 단순히 모델과 뷰 사이의 중간자 역할을 수행하고 입력되는 데이터를 로직에 맞게 처리하는 역할만 했습니다.
하지만 앞으로의 MVC패턴 즉, Spring MVC나 웹 애플리케이션에서 사용되는 컨트롤러는 이러한 기능에 더해 HTTP요청을 분석하고, 사용자 인증을 처리하고 입력 데이터를 검증하고 최종적으로 클라이언트에게 HTTP응답을 반환하는 기능을 수행합니다.
여러가지의 기능을 수행하다보니 단순히 '제어자'라는 개념의 컨트롤러보다는 '핸들링처리를 한다'라고 하여 핸들러라고 명시해 컨트롤러의 개념을 명확하고 확대하기위해 핸들러라고 바꿔서 칭하게 되었습니다.
HandlerAdapter
핸들러 어댑터는 프론트 컨트롤러와 실제 비즈니스 로직을 수행하는 핸들러 사이 중간에서 연결다리 역할을 한다.
여기서 비즈니스 로직을 수행하는 핸들러란 이전에 설계했던 save,list,new-form을 수행하는 구현된 컨트롤러를 의미한다
public class MemberSaveV4 implements ControllerV4{ MemberRepository memberRepository = MemberRepository.getInstance(); @Override public String process(Map<String, String> pramMap, Map<String, Object> model) { String name = pramMap.get("name"); int age = Integer.parseInt(pramMap.get("age")); Member member = new Member(name, age); memberRepository.save(member); model.put("member", member); return "save"; } }
위에 코드에서 비즈니스 로직의 반환타입은 String이지만 개발과정 중에 View타입으로 반환받아야하는 경우가 생겼다고 가정해보자
이 때 HandlerAdapter를 통해서 사용하고자하는 로직의 버전을 변경하고 연결해주는 역할을 핸들러 어댑터가 수행해준다.
이렇게 Adapter패턴을 이용하여 확장성있는 소프트웨어 설계를 할 수 있으며 OCP원칙을 만족하는 설계가 가능하다.
먼저 핸들러 어댑터의 인터페이스이다. 구조만 확인하고 자세한 내용은 구현체에서 확인하는 것이 이해하기 더 쉬울것이다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws ServletException, IOException;
}
V3는 실제 컨트롤러에서 View를 반환하여 View를 프론트컨트롤러에서 받아 랜더링 처리했던 컨트롤러버전이다.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter{
@Override
public boolean supports(Object handler) {
//파라미터로 받은 handler가 ControllerV3타입 또는 자식인지 확인
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView modelView = controller.process(paramMap);
return modelView;
}
private Map<String, String> createParamMap(HttpServletRequest req) {
Map<String, String> paramMap = new HashMap<>();
req.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
return paramMap;
}
}
supprots()메서드를 통해 사용하고자하는 handler를 파라미터로 받아서 해당 파라미터 핸들러가 사용하려는 컨트롤러 버전과 맞는지 검증하는 메서드이다.
이 값이 false이면 예외처리를 통해 일치하지 않는 컨트롤러이므로 사용될 수 없음을 알려야한다.
handler()메서드에서 파라미터로 받은 handler는 이미 supports메서드를 통해 검증된 뒤 들어올것이기 때문에 문제없이 타입변환을 해줄 수 있다.
그런 다음에는 이전 포스팅과 다를 것 없는 로직으로 요청에대한 파라미터를 Map에 담아 전달하고 전달한 파라미터Map을 Model로 반환받기 위해 ModelView타입으로 반환 받은 뒤 프론트 컨트롤러로 다시 반환해준다.
V3버전에 맞는 어댑터를 생성해줬으니 이제 그에 맞게 프론트 컨트롤러도 수정해보자.
private Map<String, Object> handlerMappingMap = new HashMap<>();
먼저 핸들러를 매핑하는 Map컬렉션으로 이전에는 아래와 같은 코드의 형태였다.
private Map<String, ControllerV4> controllerMap = new HashMap<>();
컨트롤러(핸들러)를 확장성있고 유연하게 매핑하기위해서 ControllerV4와 같이 컨트롤러의 버전을 명시해주던 제너릭을 Object로 받아서 핸들러어댑터의 handler메서드를 통해 컨트롤러 버전에 맞도록 다운 캐스팅되는 것을 이전에 확인했었다.
다음은 리스트 컬렉션타입의 필드인 handlerAdapters변수이다.
private List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
이 리스트는 핸들러 어댑터를 매핑하기위한 리스트 컬렉션이다.
위 두 필드는 아래코드와 같이 생성자를 통해 생성되는 시점에 매핑정보를 주입한다.
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerV5(){
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front/mvc5/v3/new-form", new MemberFormV3());
handlerMappingMap.put("/front/mvc5/v3/save", new MemberSaveV3());
handlerMappingMap.put("/front/mvc5/v3/members", new MemberListV3());
handlerMappingMap.put("/front/mvc5/v4/new-form", new MemberFormV4());
handlerMappingMap.put("/front/mvc5/v4/save", new MemberSaveV4());
handlerMappingMap.put("/front/mvc5/v4/members", new MemberListV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
위의 코드는 '확장성 있는 설계'를 통해 오직 initHandlerMappingMap()에는 기능이 구현된 컨트롤러의 매핑 정보를 넣고,
initHandlerAdapters()에는 구현된 컨트롤러의 버전을 연결해주는 Adapter만 추가해주면 메인코드의 변경없이(버전에 맞는 Adapter는 구현해야함)어떤 버전의 컨트롤러이던 확장할 수 있음을 알 수 있다.
이 후에 코드들은 주석처리된 내용만으로 설명히 충분할 것이라 생각한다.
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//POINT 핸들러 = 컨트롤러
Object handler = getHandler(request);
//POINT 가져온 핸들러에 맞는 핸들러어댑터를 찾는다
MyHandlerAdapter adapter = getHandlerAdapter(handler);
//POINT 매핑된 어댑터를 통해서 ModelView를 반환받는다.
ModelView modelView = adapter.handle(request, response, handler);
String viewName = modelView.getViewName();
MyView view = viewResolver(viewName);
view.rend(modelView.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);
}
결과적으로 보자면 이전에는 프론트 컨트롤러에서 Save, List, Form 기능에 맞는 컨트롤러를 직접호출해서 사용했었다.
여기서 '컨트롤러의 버전을 유연하게 사용하자'라는 요구사항이 발생했고 그 요구사항을 충족하기 위해서 컨트롤러의 버전을 V1, V4, V3등 여러 버전으로 왔다갔다하기위한 Adapter역할만 추가해주었을 뿐 복잡하게 생각할 필요 없을 것 같다.
Adapter는 컨트롤러(핸들러)를 유연하게 사용하기위해서 프론트컨트롤러와 실제 컨트롤러(핸들러)사이에서 말그대로 '어댑터'역할을 수행해줄 뿐이다.
위 정리한 내용들은 모두 인프런에서 김영한님의 강의를 듣고 학습한 내용을 스스로 정리한 것임을 밝힙니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1#
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!