[스프링 MVC 1편] - 4

스프링 MVC - 프론트 컨트롤러 패턴

시작

MVC 프레임워크 만들기

이전 서블릿 + JSP 조합은 공통 처리가 어려웠다. 프론트 컨트롤러를 도입하면서 MVC 프레임워크를 만들고 점점 업그레이드 해나가 스프링 MVC와 유사한 구조로 구현해보도록 하자.

Spring-Basic-capture1
위 그림에서 기존 공통 부분은 컨트롤러마다 존재했다. 프론트 컨트롤러 도입 후에는 수문장 역할을 하여 공통 부분을 해소해준다.

Spring-Basic-capture2

프론트 컨트롤러 패턴 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트 요청을 받음.
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 입구를 하나로.
  • 공통 처리 기능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨.
  • 스프링 웹 MVC 핵심도 바로 FrontController 스프링 웹 MVC DispatcherServletFrontController 패턴으로 구현되어 있음.

프톤트 컨트롤러 도입 V1

단계적으로 도입해보자. 이번 버전은 기존 코드를 최대한 유지하되 프론트 컨트롤러를 도입하는 것이다.

Spring-Basic-capture3

  • 위 그림과 같이 클라이언트는 HTTP 요청을 한다 프론트 컨트롤러는 매핑 정보를 조회하고 해당 컨트롤러를 호출한다. 그리고 컨트롤러에서는 JSP를 호출한다.
  • 포인트 컨트롤러는 인터페이스로 생성한다.

우선 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다.

java
package hello.servlet.web.frontcontroller.v1;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface ControllerV1 {

    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

그리고 이를 구현하는 각각의 컨트롤러를 작성한다.

MemberFormControllerV1

java
package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.web.frontcontroller.v1.ControllerV1;
// ...

import java.io.IOException;

public class MemberFormControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

}

MemberListControllerV1

java
package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
// ...

import java.io.IOException;
import java.util.List;

public class MemberListControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

MemberListControllerV1

java
package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
// ...

import java.io.IOException;
import java.util.List;

public class MemberListControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

MemberSaveControllerV1

java
package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
//...

import java.io.IOException;

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

        // Model에 데이터를 보관한다.
        request.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);

    }
}

이어서 프론트 컨트롤러를 작성해보자. FrontControllerServletV1

java
package hello.servlet.web.frontcontroller.v1;

import hello.servlet.web.frontcontroller.v1.ControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
// ...

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

// /front-controller/v1/ 경로로 도착한 요청은 이 컨트롤러를 우선 거쳐간다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    // 이 생성자는 WAS가 Servlet Container에서 FrontControllerServletV1을 찾아 DI 한다.
    public FrontControllerServletV1() {
        // 1. 매핑정보
        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");

        // http://localhost:8080/front-controller/v1/members/new-form
        // 위 경로에서 /front-controller/v1/members/new-form를 가져오자.
        String requestURI = request.getRequestURI();

        ControllerV1 controller = controllerMap.get(requestURI);

        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 2. 다형성의 특성을 이용해 ControllerV1 을 구현한 컨트롤러를 매핑해준다.
        controller.process(request, response);
    }
}

이렇듯 다형성의 특징으로 매핑된 컨트롤러로 오버라이드된 메서드를 호출한다.

프톤트 컨트롤러 도입 V2

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔금하지 않다.

java
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

이 부분을 깔끔히 분리하기 위해 뷰를 처리하는 객체를 만들자.

Spring-Basic-capture4

우선 위 그림의 MyView를 만들어보자

java
package hello.servlet.web.frontcontroller;

// ...

import java.io.IOException;

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

그리고 V1과 같이 인터페이스를 생성하는데, 반환 타입을 MyView로 설정한다.

java
package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
// ...

import java.io.IOException;

public interface ControllerV2 {

    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

MemberFormControllerV2

java
package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
// ...

import java.io.IOException;

public class MemberFormControllerV2 implements ControllerV2 {

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}

MemberSaveControllerV2

java
package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
// ...

import java.io.IOException;

public class MemberSaveControllerV2 implements ControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView 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);

        // Model에 데이터를 보관한다.
        request.setAttribute("member", member);

        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

MemberListControllerV2

java
package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
// ...

import java.io.IOException;
import java.util.List;

public class MemberListControllerV2 implements ControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);

        return new MyView("/WEB-INF/views/members.jsp");
    }
}

FrontControllerServletV2

java
package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
//...

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

// /front-controller/v2/ 경로로 도착한 요청은 이 컨트롤러를 우선 거쳐간다.
@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 {

        System.out.println("FrontControllerServletV2.service");

        String requestURI = request.getRequestURI();

        ControllerV2 controller = controllerMap.get(requestURI);

        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // new MyView("/WEB-INF/views/new-form.jsp")
        MyView view = controller.process(request, response);

        view.render(request, response);
    }
}

우선 프론트 컨트롤러를 먼저 만나고 요청한 URI를 매핑한다. 각 컨트롤러는 return new MyView("경로")를 반환하며 MyView는 다음과 같다.

java
package hello.servlet.web.frontcontroller;

// ...

import java.io.IOException;

public class MyView {
    private String viewPath; // /WEB-INF/views/new-form.jsp

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    // request, response를 넘겨 받아 JSP로 이동
    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

프톤트 컨트롤러 도입 V3

서블릿 종속성 제거 이번에는 Model을 도입해보자. 컨트롤러 입장에서 HttpServletRequestm, HttpServletResponse가 꼭 필요할까. 요청 파라미터 정보는 자바의 Map 으로 대신 넘기도록 하면 지금 구조에서 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다. 그리고 request 객체를 Model로 사용하는 대신 별도의 Model 객체를 만들어 반환하면 된다. 우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경해보자.

뷰 이름 중복 제거 컨트롤러는 뷰의 논리 이름을 반환하고 프론트 컨트롤러에서 처리하도록 변경하자.

  • /WEB-INF/views/new-form.jsp -> new-form

V3구조

Spring-Basic-capture5

ModelView 지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다. 그리고 Model 또한 request.setAttribute()를 통해 데이터를 저장해 뷰에 전달했다.

서블릿의 종속성을 제거하기 위해 Model을 직접 만들고 추가로 View를 전달하는 객체를 생성하자.

java
package hello.servlet.web.frontcontroller;

import java.util.HashMap;
import java.util.Map;

// 스프링에는 실제로 ModelAndView가 존재.
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName){
        this.viewName = viewName;
    }

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

그리고 ModelView를 생성한다. 스프링의 ModelAndView와 같은 역할을 한다고 보면 된다.

java
package hello.servlet.web.frontcontroller;

import java.util.HashMap;
import java.util.Map;

// 스프링에는 실제로 ModelAndView가 존재.
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName){
        this.viewName = viewName;
    }

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

그리고 컨트롤러 인터페이스를 생성하자. 이번에는 전과 달리 서블릿에 종속하지 않는다. ControllerV2의 경우 request, response가 거의 무의미했다.

java
package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);

}

MemberFormControllerV3

java
package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberFormControllerV3 implements ControllerV3 {

    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}

MemberSaveControllerV3

java
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);
        return mv;
    }
}

MemberListControllerV3

java
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.List;
import java.util.Map;

public class MemberListControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);

        return mv;
    }
}

FrontControllerServletV3

java
package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
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 java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3() {
        controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("FrontControllerServletV3.service");

        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI);

        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // request를 통해 요청 파라미터를 읽어 paramMap에 적재
        Map<String, String> paramMap = createParamMap(request);
        
        // 매핑된 컨트롤러에 paramMap을 인자로 넘기고 Model을 포함하고, 논리 JSP 이름을 가진 ModelView 반환
        ModelView mv = controller.process(paramMap);

        // 논리 이름을 물리 이름으로 변경
        String viewName = mv.getViewName(); // 논리 이름 new-form
        // /WEB-INF/views/new-form.jsp
        MyView view = viewResolver(viewName);

        // ModelView를 인자로 넘기고 View에 포함된 이름의 JSP 포워드 후 렌더링
        view.render(mv.getModel(), request, response);
    }

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

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

createParamMap를 통해 요청 파라미터를 추출한 후 논리 이름을 반환하는 컨트롤러를 호출하고 viewResolver를 통해 논리 이름 물리 이름으로 반환받는다. 그리고 아래에서 새로 생성한 MyView render 메서드를 이용해 JSP를 호출하는데, Model도 같이 담아준다.

java
// MyView

public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    model.forEach((key, value) -> modelToRequestAttribute(request, key, value));
    RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
    dispatcher.forward(request, response);
}

private static void modelToRequestAttribute(HttpServletRequest request, String key, Object value) {
    request.setAttribute(key, value);
}
  • 우선 서블릿의 종속성이 사라졌다. 다만 프론트 컨트롤러가 해야할 일은 증가했다.

단순하고 실용적인 컨트롤러 V4

V3의 경우 구조적으로 잘 만들어지긴 했지만 개발자 입장에서 항상 ModelView 객체를 생성하고 반환해야 하는 부분은 조금 번거롭다. 때문에 개발자가 사용하기 단순하고 편리하게 사용할 수 있어야 한다. 즉 실용성이 있어야 한다.

Spring-Basic-capture6

기본적인 구조는 V3와 같다. 대신 컨트롤러가 ViewName만 반환한다. 이번 버전은 인터페이스에 ModelView가 없다.

java
package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface Controller4 {

    /**
     * @param paramMap
     * @param model
     * @return viewName
     */
    String process(Map<String, String> paramMap, Map<String, Object> model);

}

MemberFormControllerV4

java
package hello.servlet.web.frontcontroller.v4.controller;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v4.ControllerV4;

import java.util.Map;

public class MemberFormControllerV4 implements ControllerV4 {

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        return "new-form";
    }
}

MemberListControllerV4

java
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.v4.ControllerV4;

import java.util.List;
import java.util.Map;

public class MemberListControllerV4 implements ControllerV4 {

    private MemberRepository memberRepository = MemberRepository.getInstance();


    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {

        // Model
        List<Member> members = memberRepository.findAll();

        model.put("members", members);

        // ViewName
        return "members";
    }
}

MemberSaveControllerV4

java
package hello.servlet.web.frontcontroller.v4.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
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) {

        // 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);

        // ViewName
        return "save-result";

    }
}

FrontControllerServletV4

java
package hello.servlet.web.frontcontroller.v4;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
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 java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@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 {

        System.out.println("FrontControllerServletV4.service");

        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<>();

        // paramMap -> request 파라미터 / model -> 파라미터를 담는 Model
        // 컨트롤러 내부에서 model의 값을 적재하고 컨트롤러는 viewName을 반환해준다. Model에 대한 처리는 끝난다.
        String viewName = controller.process(paramMap, model);

        // /WEB-INF/views/new-form.jsp
        MyView view = viewResolver(viewName);

        view.render(model, request, response);
    }

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

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

유연한 컨트롤러(어댑터) V5

이전 단계까지는 인터페이스 사용에 제약이 존재한다. 어떤 개발자는 ControllerV3로 개발하고 싶어하고 다른 개발자는 ControllerV4로 개발하고 싶어한다. 하지만 지금 단계에서는 이 요구를 충족하지 못한다.

java
private Map<String, ControllerV4> controllerMap = new HashMap<>();

어댑터 패턴 지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 이용할 수 있다.
ControllerV3ControllerV4는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다. 호환을 맞추기 위해 어댑터 패턴을 적용해보자

Spring-Basic-capture7

순서

  • 클라이언트 요청 -> 핸들러(컨트롤러) 조회 -> 핸들러 어댑터 목록(Controller3, Controller4 중 호환되는 어댑터)조회 -> 핸들러 어댑터에게 핸들러(컨트롤러) 전달 -> 핸들러 호출 -> ModelView 반환 -> 이후 단계는 같음.

핸들러 어댑터

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

핸들러

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

MyHandlerAdapter

java
package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
// ...

import java.io.IOException;

public interface MyHandlerAdapter {

    // 어뎁터가 지원할 수 있는 핸들러인지 판단
    boolean supports(Object handler);

    // 핸들러(컨트롤러)는 ModelView를 반환. 반환하지 않는다면 어뎁터가 직접 반환
    ModelView handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

ControllerV3HandlerAdapter

java
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 java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        // MemberFormControllerV3...
        return (handler instanceof ControllerV3);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        // 4. 핸들러 호출 MemberFormControllerV3...
        // supports를 먼저 호출할 것이므로 검증완료. 다운캐스팅.
        ControllerV3 controller = (ControllerV3) handler;

        // 요청 파라미터 키-값 담아놓음.
        Map<String, String> paramMap = createParamMap(request);

        // ControllerV3 구현체(MemberFormControllerV3...)는 Model과 viewName을 반환
        ModelView mv = controller.process(paramMap);

        // 5. ModelView 반환
        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;
    }
}

FrontControllerServletV5

java
package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
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.ControllerV4;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
// ...

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

//    private Map<String, ControllerV4> controllerMap = new HashMap<>();

    // 이제 어떤 핸들러를 받을 수 있도록 Object
    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());
    }
    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1. 핸들러 매핑 정보
        // 요청한 핸들러를 찾아옴. MemberFormControllerV3... MemberSaveControllerV3...
        Object handler = getHandler(request);

        if(handler == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 어댑터
        // 2. 호환돠는 핸들러 검색 ControllerV3HandlerAdapter...
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        // 3. 핸들러
        // 호환 검증 완료 호환 되지 않는 경우 IllegalArgumentException throw
        // 5. ModelView 반환
        ModelView mv = adapter.handle(request, response, handler);


        // 6, 7. viewRosolver를 통해 물리 경로
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

        // Last. JSP 이동 렌더링
        view.render(mv.getModel(), request, response);
    }

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        // 전달된 핸들러가 어댑터에서 호환되는지 확인... ControllerV3... ControllerV4
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if(adapter.supports(handler)){
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
}

유연한 컨트롤러(어댑터) V5 - 2

이번엔 V4를 붙여보자. 최종 완성 코드는 다음과 같다. 그림을 다시 보며 코드와 대조해보자.

Spring-Basic-capture7

ControllerV4HandlerAdapter

java
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 java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        // MemberFormControllerV4...
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        // 4. 핸들러 호출 MemberFormControllerV4...
        // supports를 먼저 호출할 것이므로 검증완료. 다운캐스팅.
        ControllerV4 controller = (ControllerV4) handler;

        // V4는 paramMap, model을 인자로 받음.
        Map<String, String> paramMap = createParamMap(request);
        HashMap<String, Object> model = new HashMap<>();

        // ControllerV4 구현체(MemberFormControllerV4...)는 viewName을 반환
        String viewName = controller.process(paramMap, model);

        // 어댑터가 ModelView를 반환할 수 없다면 직접 세팅
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        // 5. ModelView 반환
        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;
    }
}

FrontControllerServletV5

java
package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
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.ControllerV4;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

//    private Map<String, ControllerV4> controllerMap = new HashMap<>();

    // 이제 어떤 핸들러를 받을 수 있도록 Object
    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());

        // 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. 핸들러 매핑 정보
        // 요청한 핸들러를 찾아옴. MemberFormControllerV3... MemberFormControllerV4...
        Object handler = getHandler(request);

        if(handler == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 어댑터
        // 2. 호환돠는 핸들러 검색 ControllerV3HandlerAdapter... ControllerV4HandlerAdapter...
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        // 3. 핸들러
        // 호환 검증 완료 호환 되지 않는 경우 IllegalArgumentException throw
        ModelView mv = adapter.handle(request, response, handler);

        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

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

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        // 전달된 핸들러가 어댑터에서 호환되는지 확인... ControllerV3HandlerAdapter... ControllerV4HandlerAdapter
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if(adapter.supports(handler)){
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
}

이제 기능을 확장하더라도 코드를 변경할 필요는 없어진다. 위의 코드에서는 환경설정 정도만 추가된것이다. OCP를 만족하는것. initHandlerMappingMap, initHandlerAdapters 만 외부에서 주입시키면 된다.

또한 어댑터가 호출하는 Controller4는 뷰의 이름을 반환한다. 그런데 어댑터는 뷰의 이름이 아닌 ModelView를 만들어 반환해야 한다.

정리

V1: 프론트 컨트롤러 도입

  • 기존 구조를 최대한 유지하면서 프론트 컨트롤러 도입

V2: View 분류

  • 단순 반복 되는 뷰 로직 분리 (MyView)

V3: Model 추가

  • 서블릿 종속성 제거
  • 뷰 이름 중복 제거

V4: 단순하고 실용적인 컨트롤러

  • V3와 비슷
  • 구현 입장에서 ModelView를 직접 생성해 반환하지 않음.

V5: 유연한 컨트롤러

  • 어댑터 도입
  • 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계

여기에 애노테이션을 사용해 컨트롤러를 더 편리하게 발전시킬 수 있다. 만약 애노테이을 사용해 컨트롤러를 편리하게 사용할 수 있게 하려면 애노테이션을 지원하는 어댑터를 추가하면 된다. 다형성과 어댑터 덕분에 기존 구조를 유지하면서 프레임워크의 기능을 확장할 수 있다.

스프링 MVC
여기서 더 발전시키면 좋겠지만, 스프링 MVC의 핵심 구조를 파악하는데 필요한 부분은 모두 만들어보았다.