Lined Notebook

[스프링 MVC - 백엔드 웹 개발 기술] 10. MVC 프레임워크 만들기

by ymkim

01. 프론트 컨트롤러 패턴 소개

이번 시간에는 MVC 패턴을 개선하는 시간을 가져보자.

키워드는 공통 로직을 갖는 각각의 컨트롤러를 추상화하는 것.

01-1. 프론트 컨트롤러 패턴 도입 전

  • 이전 프론트 컨트롤러(Front Controller) 도입 전에는 각 컨트롤러가 하나의 공통 로직을 가지고 있었음
  • 이러한 부분은 “코드의 중복” 이라 볼 수 있기 때문에 좋은 구조가 아님

😶 그렇다면 이러한 중복을 어떻게 없앨 수 있는 것일까?.. 그것은 바로 프론트 컨트롤러 패턴! 스프링에서는 프론트 컨트롤러(Front Controller) 패턴을 도입하여 이러한 중복을 추상화 하였다. 다음으로 프론트 컨트롤러에 대해 간단히 알아보고 넘어가자.

01-2. 프론트 컨트롤러 패턴 도입 후

  • 프론트 컨트롤러 도입 후 공통 로직은 1개만 존재하고 Front Controller가 각각의 요청을 컨트롤러로 전달
  • Spring MVC에서는 DispatcherServelet이 해당 역할을 수행 한다

01-3. Front Controller 패턴 특징

  • “프론트 컨트롤러 서블릿” 하나로 클라이언트의 모든 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 입구를 하나로 만들 수 있으며, 공통 처리가 가능해진다
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
    • Front Controler 가 서블릿 역할을 수행함
  • 스프링 웹 MVCDispatcherServletFrontController 패턴으로 구현 돼 있음

02. 프론트 컨트롤러 도입 - v1

이번 시간에는 프론트 컨트롤러(Front Controller)를 단계적으로 도입해보는 시간을 갖는다.

목표는 기존 코드를 최대한 유지하면서, 프론트 컨트롤러(Front Controller)를 도입하는 것이다.

02-1. V1 구조

위 내용은 실제 Spring MVC의 DispatcherServlet의 동작 과정과 상당히 유사한 점을 갖고 있다. 하지만 강의에서는 해당 부분을 실제 만들어보면서 DispatcherServlet의 동작 원리를 좀 더 깊게 이해시키고자 하는 의도가 있다.

  1. 클라이언트가 HTTP 요청을 수행 한다
  2. Front Controller가 가장 먼저 해당 요청을 받는다
  3. HTTP URL 매핑 정보를 저장하고 해당 정보를 통해 컨트롤러(Controller)를 조회한다
  4. 후에 다시 한번 매핑 정보를 뒤져서 실제 URL 정보에 맞는 컨트롤러(Controller)를 호출 한다
  5. 마지막으로 JSP 페이지를 반환 한다

02-2. ControllerV1

package hello.servlet.web.frontcontroller.v1;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV1 {

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

}
  • 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다
  • 각 컨트롤러들은 이 인터페이스를 구현하면 된다
  • 프론트 컨트롤러(Front Controller)는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 갖는다

회원 폼, 저장, 목록 컨트롤러의 경우 기존 로직과 동일하며 ControllerV1을 구현한다는 차이점만 존재하기 때문에 소스에 대한 자세한 설명은 생략하고 넘어간다.

02-3. MemberFormControllerV1

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

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

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response); // 다른 서블릿이나 JSP로 이동할 수 있는 기능, 서버 내부에서 다시 호출 발생
    }
}
  • 회원 폼 컨트롤러

02-4. MemberSaveControllerV1

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 javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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에 데이터를 보관해야 한다
        // ex) model.setAttribute("member", member);
        request.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response); // 서버 내부에서 호출
    }
}
  • 회원 등록 컨트롤러

02-5. MemberListControllerV1

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 javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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);
    }
}
  • 회원 목록 컨트롤러

02-6. FrontControllerServletV1

package hello.servlet.web.frontcontroller.v1;

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

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

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

    public FrontControllerServletV1() {
        this.controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        this.controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        this.controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServlet.service");

        String requestURI = request.getRequestURI();
        ControllerV1 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controller.process(request, response);
    }
}
  • 프론트 컨트롤러(FronController)
  • /front-controller/v1/* : /front-controller/v1/ 로 시작되는 요청은 해당 서블릿이 처리
  • Map의 Key로는 URLValue에는 ControllerV1 인터페이스를 지정한다
  • RequestURI에 매칭되는 정보가 있는 경우 해당 객체(controller)의 process 메서드를 호출
  • 다형성 원칙에 따라서 오버라이딩 된 process 메서드가 호출되는 점이 중요하다

03. View 분리 - V2

public class MemberFormControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response); // forward -> 서버 내부적으로 요청이 돈다			
    }
}

이번 시간에는 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있는 부분을 제거할 것이다.

03-1. V2 구조

V1 구조

  • 이전에는 FrontController에서 View 페이지를 반환하는 형식이 아니라 각각의 컨트롤러에서 viewPath를 지정한 후 해당 경로에 맞춰서 forward 하는 방식을 사용하였다. 하지만 이러한 불필요한 중복은 필요하지 않기 때문에 V2 구조에서는 View 페이지는 따로 분리한다.

V2 구조

  • V1 구조를 보면 이전에는 Controller에서 JSP로 바로 Forward 하는 구조였다
  • V2 구조에서는 Controller에서 직접 Forward 하지 않고 MyView라는 객체를 반환하고 FrontController가 MyView를 반환하는 구조로 만들 것이다

03-2. Myview

package hello.servlet.web.frontcontroller;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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);
    }
}
  • viewPath 라는 인스턴스 변수를 갖는 MyView 클래스를 생성한다
  • 해당 객체는 각각의 컨트롤러 안에 존재하였던 forward 관련 로직을 render라는 함수를 통해 구현하였다

03-3. ControllerV2

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV2 {

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

}
  • 이번에는 void 형식이 아닌 MyView 객체를 반환하는 추상메서드를 추가한다

03-3. MemberFormControllerV2

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

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

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MemberFormControllerV2 implements ControllerV2 {

    @Override
    public MyView 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); // forward -> 서버 내부적으로 요청이 돈다		
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}
  • MyView 객체 생성 시 물리적인 화면 경로를 인수로 넣어준다
  • 위와 같은 방법을 통해 이전에 존재 하였던 불필요한 로직 제거가 가능해진다

03-4. MemberSaveControllerV2

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 javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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에 데이터를 보관해야 한다
        // ex) model.setAttribute("member", member);
        request.setAttribute("member", member);

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

03-5. MemberListControllerV2

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

03-6. FrontControllerServletV2

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
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 javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

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

    public FrontControllerServletV2() {
        this.controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        this.controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        this.controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServlet.service");

        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);
    }
}
  • controller.process(request, response)를 통해 바로 JSP에 반환하는 것이 아니라 MyView 객체를 받은 후 해당 객체의 메서드인 render(request, response) 함수를 호출한다. 이렇게 하면 View에 관련된 로직을 분리할 수 있다.

04. Model 추가 - V3

04-1. 서블릿 종속성 제거

  • 컨트롤러 입장에서 HttpServletRequest, HttpServletResponse가 꼭 필요할까?
  • 요청 파라미터 정보는 자바의 Map이나 DTO를 사용하면, 현 구조에서 서블릿을 몰라도 동작이 가능하다
  • 또한 request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다
  • 현재 구현하는 컨트롤러들이 서블릿 기술을 전혀 사용하지 않도록 변경해보자
  • 이와 같이 하면 구현 코드가 단순해지고, 테스트 코드 작성이 쉬워진다

04-2. 뷰 이름 중복 제거

  • 컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있음
    • return new MyView("/WEB-INF/views/뷰이름.jsp");
  • 컨트롤러뷰의 논리 이름을 반환하고, 실제 물리 위치 이름은 프론트 컨트롤러에서 처리하도록 단순화한다
  • 이렇게 하면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다
    • /WEB-INF/views/new-form.jsp → new-form
    • /WEB-INF/views/save-result.jsp → save-result
    • /WEB-INF/views/members.jsp → members

04-3. V3 구조

  1. Client가 HTTP 요청을 수행
  2. FrontController(DispatcherServlet)은 해당 요청 URL 정보 저장, 실제 컨트롤러(Controller)를 찾아 호출한다
  3. 컨트롤러(Controller)는 요청을 처리하고 ModelView를 만들어서 FrontController에게 전달한다
  4. FrontController는 ViewResolver를 호출하고, ViewResolver가 MyView를 FrontController에게 다시 반환한다
  5. 마지막으로 FrontController는 해당 정보를 render하는 흐름으로 작성된다

ModelView

  • 현재까지는 컨트롤러에서 서블릿에 종속적인 HttpServletRequest, HttpServletResponse를 사용 하였으며 Model도 request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달 하였다
  • 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, View 이름까지 전달하는 객체를 만들어야 함
  • 이번 버전(V3)에서는 HttpServletRequest 사용도 불가능하고, request.setAttribute()을 통한 Model 사용도 불가능하다는 전제로 로직을 만든다, 따라서 Model이 별로도 필요하다

04-4. ModelView

package hello.servlet.web.frontcontroller;

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

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;
    }
}
  • 스프링(Spring)에서는 기본적으로 제공되는 ModelAndView가 존재함
  • 이번에는 ModeView라는 viewName과 model 데이터를 저장하는 객체를 만든다

04-5. ControllerV3

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

}
  • 이번에는 ModelView를 반환하는 추상 메서드를 생성한다

04-6. MemberFormControllerV3

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");
    }
}
  • 회원 폼 컨트롤러의 경우 논리 이름인 new-form만 생성자에 넣어서 반환

04-7. MemberSaveControllerV3

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;
    }
}
  • 회원 등록 컨트롤러의 경우 save-result라는 논리 view명을 생성자에 넣는다
  • 후에 데이터 전달을 위해 mv.getModel.put(”member”, member);에 값을 저장한다
    • Map<String, Object> model = new HashMap<>();
  • 마지막으로 해당 mv를 반환한다

04-8. MemberListControllerV3

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;
    }
}
  • 회원 조회 컨트롤러의 경우 members라는 논리 view명을 생성자에 넣는다

04-9. FrontControllerServletV3

package hello.servlet.web.frontcontroller.v3;

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 javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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() {
        this.controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        this.controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        this.controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServlet.service");

        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        String viewName = mv.getViewName(); // 논리이름 new-form 만 추출이 된다
        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 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(HttpServletRequest request)
    • HttpServletRequest에서 파라미터 정보를 꺼내 Map으로 변환하는 함수
    • 해당 Map(paramMap)을 컨트롤러에 전달하면서 호출 한다
  • 뷰 리졸버
    • Myview view = viewResolver(viewName)
    • 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다
    • 실제 물리 경로가 있는 MyView 객체를 반환한다
    • 논리 뷰 이름 : members(전체 회원 조회)
    • 물리 뷰 이름 : /WEB-INF/views/members.jsp
  • JSP는 request.getAttribute()로 데이터를 조회하기에 모델의 데이터를 꺼내서 request.setAttribute에 담아둔다
  • JSP로 포워드 해서 JSP를 랜더링 한다

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

앞서 만든 V3 컨트롤러서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다.

하지만 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 보면, 항상 ModelView 객체를 생성하고 반환해야

하는 부분은 조금 번거롭다.

 

좋은 프레임워크는 아키텍처도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다, 소위 실용성이 있어야 한다. 이번에는 v3를 조금 변경하여 개발자들이 편하도록 만들어보자.

05-1. v4 구조

  • 기본적인 구조는 V3와 동일
  • ModelView를 반환 안하고, viewName만 반환
    • return “new-form”

05-2. ControllerV4

package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {

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

}
  • 기존에는 Map<String, String> paramMap만 받았는데, V4 버전에서는 model 파라미터도 같이 받는다
  • model은 하위 구현 클래스에서 셋팅 한다

05-3. MemberFormControllerV4

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

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 ModelView("new-form"); // AS-IS
        return "new-form";
    }
}
  • 기존 new ModelView(”view명”)이 사라지고 String 형태의 “new-form”만 남긴다

05-4. MemberSaveControllerV4

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) {
        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); // call by reference
        return "save-result";
    }
}
  • 파라미터 인자로 넘어온 model 객체에 member 값을 셋팅한다
  • Map<String, Object>의 경우 call by reference(참조 자료형)이기에 값을 변경하면 원본 값이 변경 됨
  • return “save-result” 사용

05-5. MemberListControllerV4

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.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) {
        List<Member> members = memberRepository.findAll();
        model.put("members", members);
        return "members";
    }
}
  • return “members” 사용

05-6. FrontControllerServletV4

@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {

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

    public FrontControllerServletV4() {
        this.controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        this.controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        this.controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServlet.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<>();
        String viewName = controller.process(paramMap, model);

        //String viewName = mv.getViewName(); // 논리이름 new-form 만 추출이 된다
        MyView view = viewResolver(viewName);

        //view.render(mv.getModel(), request, response);
        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;
    }
}
  • 기존 구조는 v3와 완전하게 동일함
  • 달라진 부분은 model 변수를 인수로 넘겨주는 부분과, process 호출 시 view 이름(String)으로 받는 부분이다

06. 유연한 컨트롤러1 - V5

만약 어떤 개발자는 ControllerV3 방식으로 만들고 싶고, 어떤 개발자는 ControllerV4 방식으로 만들고 싶은 경우 어떻게 해야할까?

// V3
public interface ControllerV3 {
    ModelView process(Map<String, Object> paramMap);
}
  • 개발자 A는 V3 방식으로 개발을 하고 싶은 상황
// V4
public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}
  • 개발자 B는 V4 방식으로 개발을 하고 싶은 상황

06-1. 어댑터 패턴

템플릿 메서드 패턴, 팩토리 메서드 패턴, 전략 패턴, 싱글턴 패턴… 다양한 디자인패턴이 존재함
참고 : 자바 어댑터 패턴은 어떻게 쓰일까?

  • 지금까지 우리가 만든 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용 가능
  • ControllerV3, ControllerV4는 완전 다른 인터페이스여서 호환이 불가능한 상태이다
    • 마치 v3는 110v이고, v4는 220v 전기 콘센트 같은 느낌임 😶
    • 이럴 때 사용하는 것이 어댑터이다 (전기 중간 연결 포인트)
  • 어댑터 패턴사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경한다
    • 어댑터 패턴 : 호환되지 않는 인터페이스들을 연결하는 디자인패턴

06-2. V5 구조

  • 핸들러 어댑터
    • 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다
    • 여기서 어댑터 역할을 해주기 때문에 다양한 종류의 컨트롤러 호출이 가능해진다
  • 핸들러
    • 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경하였다
    • 이유는 이제 어댑터가 있기 때문에 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리가 가능하기 떄문이다

06-3. MyHandlerAdapter

public interface MyHandlerAdapter {
		
    boolean supports(Object handler); // Object handler -> new MemberListControllerV3

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;

}
  • boolean supports(Object handler)
    • 여기서 handler는 컨트롤러를 말한다
    • 어탭터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드
  • ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
    • 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다
    • 실제 컨트롤러가 ModelView를 반화하지 못하면, 어댑터가 ModelView를 직접 생성해서라고 반환해야 함
    • 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해 실제 컨트롤러가 호출된다

06-4. 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);
    }

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

        Map<String, String> paramMap = createParamMap(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(Object handler)
    • 매개변수로 들어온 handler가 ControllerV3 타입인지 검증
  • handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException
    • 실제 컨트롤러를 호출해주는 핸들러 어댑터 역할 담당

06-4. FrontControllerServletV5

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.v5.adapter.ControllerV3HandlerAdapter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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 final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    public FrontControllerServletV5() {
        initHandlerMappingMap();
        initHandlerAdapters();
    }

    private void initHandlerMappingMap() {
        this.handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        this.handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        this.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 {

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

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

        String viewName = mv.getViewName(); // 논리이름 new-form 만 추출이 된다
        MyView view = viewResolver(viewName);

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

    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        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);
    }

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
  • getHandler(request)
    • request URI에 매핑된 객체를 가져온다
    • 여기서는 new MemberFormControllerV3 객체를 가져오게 된다
  • getHandlerAdapter(handler)
    • 위에서 나온 hanlder(MemberFormControllerV3) 객체를 해당 함수에 전달 한다
    • 해당 함수는 MemberFormControllerV3가 ControllerV3의 인스턴스인지 검증
    • 인스턴스 타입에 해당이 된다면 true를 반환, 그렇지 않으면 예외 발생
  • adapter.handle(request, response, handler)
    • 모든 요청을 핸들러 어댑터에게 전달 한다
    • 해당 핸들러 어댑터가 실제 컨트롤러를 호출 한다

07. 유연한 컨트롤러2 - V5

이전 시간에는 사용자가 원하는 인터페이스에 따라서 교체를 할 수 있는 어댑터 패턴을 사용해 보았다.

이번에는 FrontControllerV5에 ControllerV4 기능을 추가해보자.

07-1. 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() {
        this.handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        this.handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        this.handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

        // V4 버전 추가
        this.handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        this.handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        this.handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

		...중략

		private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter()); // v4 버전 추가
    }

		...중략
}
  • v4 버전도 initHandlerMappingMap에 추가한다
  • initHandlerAdapters에는 new ControllerV4HandlerAdapter()를 추가한다
  • 위와 같이 추가를 하면 OCP(개방 폐쇄 원칙)을 준수할 수 있다

07-2. ControllerV4Adapter

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);
        Map<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;
    }
}
  • supports 함수의 경우 기존 로직과 동일함, ControllerV4의 인스턴스인지만 확인
  • handle 함수의 경우 기존 로직과 동일함
    • 여기서는 ModelView mv = new ModelView(viewName) 이 중요 로직이다
    • controller.process를 하는 경우 view 이름 ( “save-result” ) 이런식으로만 반환 하지만 현재 리턴 타입이 ModelView이기에 해당 객체를 생성하여 반환 한다

08. 정리

  • 지금까지 v1 ~ v5로 점진적으로 F/W를 발전시켜 왔음
  • 지금까지 한 작업을 정리 해보자

08-1. v1 프론트 컨트롤러 도입

  • 기존 구조를 최대한 유지하면서 프론트 컨트롤러 도입
  • 기존에는 사용자의 요청에 맞게 1:1로 매핑 하여 공통로직이 각 컨트롤러에 존재하였다. 하지만 프론트 컨트롤러 도입 후에는 공통 로직은 FrontController에 선언하고 각각의 컨트롤러는 FrontController에 의해 호출되는 방식으로 처리가 되었다

08-2. v2 view 분류

String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response); // 다른 서블릿이나 JSP로 이동할 수 있는 기능, 서버 내부에서 다시 호출 발생
  • 단순 반복되는 뷰 로직을 분리 하였다
  • 위와 같이 컨트롤러마다 반복되는 뷰 로직을 MyView로 분리 하였다

08-3. v3 Model 추가

  • 서블릿 종속성 제거
    • HttpServletRequest, HttpServletResponse 제거
  • 뷰 이름 중복 제거
    • prefix, subfix를 사용하여 뷰 중복 제거

08-4. v4 단순하고 실용 적인 컨트롤러

  • v3와 거의 비슷
  • 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공

08-5. v5 유연한 컨트롤러

  • 어댑터 도입
  • 어댑터를 추가해서 F/W를 유연하고 확장성 있게 설계

여기에 애노테이션을 사용해서 컨트롤러를 더 편리하게 발전시킬 수 있다. 만약 애노테이션을 사용해서 컨트롤러를 편리하게 사용할 수 있게 하려면 어떻게 해야할까? 바로 애노테이션을 지원하는 어댑터를 추가하면 된다! 다형성과 어댑터 덕분에 기존 구조를 유지하면서, F/W의 기능을 확장할 수 있다.

08-6. 스프링 MVC

  • 여기서 발전 시키면 좋지만, 스프링 MVC 핵심 구조를 파악하는데 필요한 부분은 다 만들어 보았음
  • 스프링 MVC는 위에서 학습한 내용과 거의 같은 구조를 가지고 있음

블로그의 정보

기록하고, 복기하고

ymkim

활동하기