본문으로 바로가기
반응형

 

 

 

 

이전 포스팅에선 컨트롤러에서 String(view name)를 반환하는 것을 다뤘다.

 

 

https://healthdevelop.tistory.com/entry/spring52

 

[Spring MVC] 프론트 컨트롤러 패턴 도입 | 프론트 컨트롤러 패턴 적용하기 - 회원 관리 예제(FrontContr

● 프론트 컨트롤러(Front Controller)  Spring MVC 에서 프론트 컨트롤러가 도입되고 나서 컨트롤러의 호출 이전에 공통 처리 기능이 가능해졌다. 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청

healthdevelop.tistory.com

 

 

프론트 컨트롤러에서 매핑 정보에 맞는 컨트롤러를 호출하고 

컨트롤러에 map 형식의 model을 넘겨주고 로직을 수행한 후

forwarding 할 view 이름을 반환하는 식으로 코드 구조를 작성했다.

 

 

이제는 완벽하게 Spring Mvc 프론트 컨트롤러의 구조에 도달했다.

하지만 마지막 한가지 구조가 남았다.

 

개발자마다 다른 컨트롤러를 사용하고자 할 때는 어떻게 해야 할까?

 

예를 들어 A 개발자는 ModelView를 반환하는 컨트롤러를 사용하고 싶어 하고

B 개발자는 String(view name)을 반환하는 컨트롤러를 사용하고 싶어 한다.

 

위 상황을 해결하기 위해 어댑터 패턴이 도입된다.

 

 

 


● 구조  

 

출처: 스프링 MVC 1편(김영한)

 

위 사진은 핸들러 패턴을 도입한 구조이다.

 

핸들러 어댑터: 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다. 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.

 

핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다

 

 

코드 작성을 통해 구조를 이해해보자.

 

 


● ModelView 생성

 

 

 

 

 

MyHandlerAdapter(인터페이스)

package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.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(Object handler)

 - 매개변수로 넘어온 핸들러가 지원 가능한 핸들러인지 확인하는 함수

   어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드다.

 

handle()

 - 가져온 핸들러 어댑터에서 handle() 메서드를 실행하여 로직을 수행한다.

   어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다.

   실제 컨트롤러가 ModelView를 반환하지 못하면 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.     이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만

   이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다

 

 

 

 

 

 

 

ControllerV3HandlerAdapter(어댑터 구현체)

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 javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3); // 핸들러(컨트롤러)가 V3 인스턴스 인지
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV3 controllerV3 = (ControllerV3) handler;

        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controllerV3.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;
    }
}

 

MyHandlerAdapter의 구현체이다.

해당 클래스는 핸들러가 ControllerV3 일 때만 수행 가능하다.

supports() 를 통해 매개변수로 받은 핸들러(Object)가 ControllerV3의 구현체 인지 확인하고,

handle() 에선 Object를 ControllerV3로 다운캐스팅 한 다음에 로직을 수행한다.

(ControllerV3의 process() 반환은 ModelView)

 

 

 

 

 

ControllerV4HandlerAdapter(어댑터 구현체)

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 javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.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 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;
    }
}

 

MyHandlerAdapter의 구현체이다.

해당 클래스는 핸들러가 ControllerV4 일 때만 수행 가능하다.

supports() 를 통해 매개변수로 받은 핸들러(Object)가 ControllerV4의 구현체 인지 확인하고,

handle() 에선 Object를 ControllerV4로 다운캐스팅 한 다음에 로직을 수행한다.

 

 

여기서 ControllerV3 어댑터와 다른 점은 아래와 같다.

 

ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;

 

ControllerV4의 process() 반환은 String(View name)이기 때문에

어댑터는 뷰의 이름이 아니라 ModelView 를 만들어서 반환해야 한다.

여기서 어댑터가 꼭 필요한 이유가 나온다. ControllerV4 는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어 반환한다

 

 

 

 

 

FrontController

package hello.servlet.web.frontcontroller.v5;

import *;

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

    // 기존 코드
    // private Map<String, ControllerV4> controllerV4Map = new HashMap<>();
    private final Map<String, Object> handlerMappingMap = new HashMap<>(); // Object로 범용성클 키움
    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());

        // 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){ // 읽어온 URI에 매치되는 Controller 객체가 없다면
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404 Not Found
            return;
        }

        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        ModelView mv = adapter.handle(request, response, handler);

        String viewName = mv.getViewName();// 논리 이름 ex. new-form
        MyView view = viewResolver(viewName);

        view.render(mv.getModel(), request, response);

    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();// 들어온 URL을 얻을 수 있다.
        return handlerMappingMap.get(requestURI);// 요청 온 URI에 의해 Controller 객체가 다르게 호출
    }

    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");
    }
}

 

이전에는 프론트 컨트롤러에서 매핑을 통해 직접적으로 컨트롤러를 사용했다.

이제는 어떠한 것이라도 url 매핑을 통해 사용할 수 있다.

 

 

 


 

 

이번 포스팅에선 어댑터를 도입하고, 추가해서 프레임워크를 유연하고 확장성 있게 설계했다.

지금까지 설계한 내용이 스프링 MVC의 구조와 거의 동일하다.

 

다음 포스팅에선 스프링 MVC 구조에 대해 알아보고자 한다.

 

 

 

 

반응형