4. MVC 프레임워크 만들기

김현준·2023년 7월 21일

Gaounuri_Spring_Study

목록 보기
4/10

Project Code File

📌 목차

  • 프론트 컨트롤러 도입 - v1

  • View 분리 - v2

  • Model 추가 - v3

  • 단순하고 실용적인 컨트롤러 - v4

  • 유연한 컨트롤러 1 - v5

  • 유연한 컨트롤러 2 - v5

📌 개요

이전에 jspservlet 만으로 MVC 패턴을 적용하였지만 여전히 중복코드가 발생했고 여러 문제점이 있었다.

이를 해결하기 위해 frontController 를 도입함으로써 중복코드를 제거한다. 버전은 총 5개가 있다.

먼저 v1 에서는 구조적으로 형태를 맞출것이므로 이전에 만들어둔 코드에 비해서 코드가 추가되거나 크게 변경되는 부분은 없다.
역할과 책임을 분류하면 중복된 코드를 다른코드에 영향이 안가도록 지울 수 있다.

그다음으로 v2 버전에서는 중복코드를 처리해주는 뷰를 도입함으로써 중복코드를 해결할 것이다.

v3 버전에서는 지금까지 사용해온 서블릿에 대해 종속성을 제거하고 뷰 이름에 대한 중복을 제거하면서 개선한다. 이로 인해 구현코드와 테스트코드 작성이 쉬워지고 경로의 변화나 확장자의 변화에 대해 유연하게 대처할 수 있다.

v4 버전에서는 필요없는 객체를 생성하고 반환하는 과정을 파라미터를 이용해 간략화 한다.

마지막으로 v5 버전에서는 어뎁터 패턴을 이용하여 원하는 컨트롤러 버전을 유연하게 사용할 수 있도록 구현한다.

📌 프론트 컨트롤러 도입 - v1

이번에는 프론트 컨트롤러를 도입함으로써 이를 해결해보자.

먼저 기존의 코드를 최대한 유지하면서 구조를 만들어 본다.

image

구조는 위 사진과 같다.

클라이언트가 http 요청을 하면 프론트 컨트롤러 라는 게이트를 거쳐가고 url 매핑정보를 가져온다음에 컨트롤러와 jsp 를 호출한다.

package hello.servlet.web.frontcontroller.v1;

public interface ControllerV1 {
    void process(HttpServletRequest request , HttpServletResponse response) throws ServletException, IOException;
}

컨트롤러는 인터페이스로 구현한다. 이제 이 인터페이스를 구현한 회원등록 컨트롤러를 살펴보자.

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


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); // 리다이렉트가 아님
    }
}

viewPath 를 선언하고 dispatcher 를 사용해서 Controller 를 통해 jsp 파일을 불러올 수 있도록 한다.

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


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")); // getParameter 의 반환값은 항상 String이다.

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

회원저장 컨트롤러는 MemberRepository 를 싱글톤으로 가져오고 request 의 파라미터를 가져온뒤 , Member 객체를 만들고 저장한다.

이제 viewPath 를 선언하고 dispatcher 객체를 만들어서 jsp 파일을 호출한다.

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

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

회원 리스트 목록 조회는 findAll 메서드를 통해 List 에 담고 viewPath 를 만들어준뒤 requestsetAttribute 에 데이터를 저장하고 dispatcher 를 호출한다.

이제 frontController 를 살펴보자.

package hello.servlet.web.frontcontroller.v1;

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

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

    public FrontControllerServletV1() {
        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 {

        String requestURI = request.getRequestURI(); // URI 정보를 받는다.

        ControllerV1 controller = controllerMap.get(requestURI);

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

        controller.process(request , response);


    }
}

기존의 servlet,jsp 를 통한 MVC 패턴과의 차이점을 본다면 frontController 에서 url 경로를 생성자를 통해 미리 지정해준다. 따라서 회원저장 , 회원폼 , 회원목록조회 구현체에는 url 링크를 가지지 않는다.

또한 controllerMap 을 통해서 하나의 공통된 컨트롤러를 거쳐서 로직을 실행하도록 만들었다.

기본적인 구조는 맞추었으나 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 발생하고 깔끔하지 않다.

이제 구조를 맞췄으니 간략화를 해보자.

📌 View 분리 - v2

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

이 부분을 중복이 발생하지 않도록 처리해주는 뷰를 만들어보자.

image

구조는 위와같다.

MyView 를 통해 랜더링을 함으로써 컨트롤러에서 랜더링을 하지 않도록 한다.

package hello.servlet.web.frontcontroller;

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 를 생성자로 받고 render 함수를 추가하였다.

package hello.servlet.web.frontcontroller.v2;

public interface ControllerV2 {
    MyView process(HttpServletRequest request , HttpServletResponse response) throws ServletException, IOException;
}

Controller 는 이제 MyView 객체를 반환한다.

이제 인터페이스를 구현해보자.

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

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

회원 등록 컨트롤러에 대한 코드는 위와 같다.
코드가 훨씬 깔끔해졌다. MyView 객체를 생성함과 동시에 경로를 지정해주고 반환해주는 역할을 한다.

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

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")); // getParameter 의 반환값은 항상 String이다.

        Member member = new Member(username , age);
        memberRepository.save(member);

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

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

회원 저장코드는 위와 같다. request 에서 parameter 로 이름과 나이를 받아오고 Member 객체를 생성하고 저장한뒤에 MyView 객체를 반환한다.

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

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

회원 목록조회코드도 깔끔하졌다. findAll 함수로 모두 찾은뒤 request 에 저장한뒤 MyView 객체를 경로와함께 반환해주면 끝이다.

package hello.servlet.web.frontcontroller.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 {

        String requestURI = request.getRequestURI(); // URI 정보를 받는다.

        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); // 추가된 코드

    }
}

frontController 를 보자. v1 버전과 비교했을때 생성자 부분은 변함이 없다.
다만 view.render(request , response) 코드가 딱 한줄 추가되었다.

역할을 분리함으로써 중복된 코드를 분리하니 frontController 코드 한줄이 추가되고 회원등록 , 회원저장 , 목록조회 코드에서 중복되는 코드가 전부 지워지면서 코드의 수가 줄었다.

이제 조금 더 역할을 분리해보자.

📌 Model 추가 - v3

개선해야할 점을 찾아보자.

  • 서블릿 종속성 제거

컨트롤러 입장에서 HttpServletRequest, HttpServletResponse 이 꼭 필요할까?
요청 파라미터 정보는 자바의 Map 으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.

그리고 request 객체를 Model 로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다. 우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경해보자.
이렇게 하면 구현 코드도 매우 단순해지고, 테스트 코드 작성이 쉽다.

  • 뷰 이름 중복 제거

컨트롤러에서 지정하는 뷰 이름이 중복되는것을 알 수 있다. 만약에 경로가 바뀌거나 확장자가 바뀌면 하나하나 바꿔야할 수 있다. 따라서 뷰의 논리 이름을 반환하고 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 하자.

  • /WEB-INF/views/new-form.jsp -> new-form
  • /WEB-INF/views/save-result.jsp -> save-result
  • /WEB-INF/views/members.jsp -> members

image

구조는 위와같다. viewResolver 가 추가되었다.
또한 Controller 에서 이제 ModelView 를 반환한다.

지금까지 request.setAttribute() 를 통해 데이터를 저장했는데 서블릿의 종속성을 제거하기 위해 데이터를 저장할 별도의 객체가 필요하다. 이를 ModelView 에서 해줄 예정이다.

package hello.servlet.web.frontcontroller;

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 코드는 위와같다. lombok 을 통해서 getter,setter 를 구현하지 않아도 된다.

package hello.servlet.web.frontcontroller.v3;

public interface ControllerV3 {

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

}

Controller 는 위와같다. ModelView 를 반환하고 파라미터는 Map<String,String> 이다. 이전의 컨트롤러에서는 HttpServletRequest , HttpServletResponse 를 모두 담았지만 이젠 그럴 필요가없다.

이제 회원등록 , 회원 저장 , 목록조회 코드를 살펴보자.

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

public class MemberFormControllerV3 implements ControllerV3 {

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

이제 객체를 반환할때 실제 물리 주소가 아니라 논리적 주소를 반환한다.
또한 메서드의 파라미터에 HttpServletRequest 가 더이상 존재하지 않고 paramMap 만 존재한다.

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

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;

    }
}

이전의 v2 버전에서는 request.setAttribute("member", member) 와 같이 데이터를 저장했지만 이젠 ModelView 를 통해 데이터를 저장을 한다.

또한 여기서도 논리적 주소인 save-result 를 사용하고 있고 파라미터 정보 또한 httpservletRequest 를 사용하지 않는다.

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

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

v2 버전에서는 request.setAttribute("members", members); 로 회원목록을 저장했지만 이제 ModelView를 통해 데이터를 저장한다.

package hello.servlet.web.frontcontroller.v3;

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

        String requestURI = request.getRequestURI(); // URI 정보를 받는다.

        ControllerV3 controller = controllerMap.get(requestURI);

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

        // paramMap 을 넘겨주어야 함

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

        // 논리적 주소를 물리적 주소로 반환해야한다.
        String viewName = mv.getViewName();

        // 아래 코드를 통해 파일의 경로나 확장자를 유연하게 대처할 수 있다.
        MyView view = viewResolver(viewName);
        view.render(mv.getModel() , request, response); // 추가된 코드

    }

    private static MyView viewResolver(String viewName) {
        // 객체를 생성하는것은 service 클래스에 준위가 맞지 않기 때문에 따로 클래스를 뽑아 주는게 좋다.
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) { // ctrl+alt+M - 람다식 메서드 반환
        Map<String, String> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
            .forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
        // paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
        return paraMap;
    }
}

frontController 코드는 위와 같다. 먼저 createParamMap 메서드를 보면 람다식을 통해 paraMaprequest 에 대한 모든 정보를 넘겨주고 반환한다.

즉 이름 , 나이 정보에 대해서 저장을 한다.

이후 paramMap 을 파라미터로 넣어주면서 ModelView 객체를 만든다. 이때 ModelView 를 통해 논리적 주소를 얻을 수 있다.

viewName 에 논리적 주소를 먼저 담고 viewResolver 메서드를 통해서 논리적 주소를 물리적 주소로 반환한다.

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

private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
    model.forEach((key,value) -> request.setAttribute(key,value));
}

이전에 사용하던 MyView 클래스에 두개의 메서드가 추가되었다.

먼저 modelToRequestAttribute 를 보면 model 을 파라미터로 넘겨받은 후에
모델에 있는 key,value 를 전부 request.setAttribute() 를 통해 저장해준다.
그리고 dispatcher 를 통해 렌더링을 해준다.

v2 버전에서는 setAttribute 를 모든 컨트롤러에서 매번 해주었지만 이젠 MyView 에서 한꺼번에 처리해준다.

이제 컨트롤러는 서블릿에 대한 종속성을 제거하고 뷰 경로의 중복을 제거할 수 있게 되었다.

📌 단순하고 실용적인 컨트롤러 - v4

v3 버전에서 Controller 코드를 보면 중복이 발생한다. 항상 ModelView 객체를 생성하고 반환해야 한다는 점이다.

구조적으로는 잘 짜여있지만 매번 컨트롤러를 만들때마다 ModelView 객체를 생성하고 반환해야한다면 어떨까?

이번에는 실용성을 늘려보자.

image

이제 ControllerModelView 를 반환하지 않고 ViewName 을 반환하게 된다. 코드를 살펴보자.

package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

따라서 Controller 인터페이스는 ModelView 객체를 반환하지 않고 String 문자열을 반환한다. 그리고 Model 은 파라미터로 넘긴다.

이제 회원 등록 , 회원 저장 , 목록 조회 코드를 구현해보자.

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

public class MemberFormControllerV4 implements ControllerV4 {

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

회원 등록 코드가 훨씬 깔끔해졌다. 이전에는 ModelView 객체를 생성하고 반환을 해야했는데 이제 논리적 주소만 반환하면 된다.

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

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

회원 저장 로직에서는 파라미터로 넘겨온 modelput 으로 값을 저장만 해주면 끝이다.
반환값은 논리주소이므로 ModelView 객체를 생성하지 않아도 된다.

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

public class MemberListControllerV4 implements ControllerV4 {

    private final 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";
    }
}

목록 조회도 코드가 더욱 깔끔해졌다.

파라미터에 정보를 넣음으로써 매번 객체를 생성해야하는 복잡한 코드를 더이상 쓰지 않아도 된다.

package hello.servlet.web.frontcontroller.v4;

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

        String requestURI = request.getRequestURI(); // URI 정보를 받는다.

        ControllerV4 controller = controllerMap.get(requestURI);

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

        // paramMap 을 넘겨주어야 함

        Map<String, String> paraMap = createParamMap(request);

        Map<String, Object> model = new HashMap<>(); // 추가

        String viewName = controller.process(paraMap, model);

        MyView view = viewResolver(viewName);

        view.render(model , request, response);

    }

    private static MyView viewResolver(String viewName) {
        // 객체를 생성하는것은 service 클래스에 준위가 맞지 않기 때문에 따로 클래스를 뽑아 주는게 좋다.
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) { // ctrl+alt+M - 람다식 메서드 반환
        Map<String, String> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
            .forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
        // paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
        return paraMap;
    }
}

frontController 코드를 살펴보자. v3 버전에 비해 바뀐점은 크게 없다.
service 메서드 안에서 직접 model 을 만들어 주는 코드 한줄만 추가되었다.

컨트롤러가 직접 뷰의 논리 주소를 반환하므로 model 객체는 한번만 생성해도 충분하다.

지금까지 단지 model 을 파라미터로 넘겼을 뿐인데 객체를 여러번 생성하는 중복코드를 지울 수 있게 되었다. 이를 통해 구조적인 변화는 크게 변하지 않았지만 실용성이 크게 증가하였다.

📌 유연한 컨트롤러1 - v5

만약 어떤 개발자는 V3 버전의 컨트롤러를 사용하고싶고 다른 개발자는 V4 버전의 컨트롤러를 사용하고 싶다면 어떡할까?

지금까지 개발한 frontController 는 하나의 컨트롤러 인터페이스만 사용할 수 있다.

이를 해결 하기 위해서는 어뎁터 패턴 이 필요하다.

110v 전기 콘센트에 220v 제품을 사용할 수 없으므로 변환 어뎁터를 사용하듯이 적용해보자.

image

이제 handler 라는 개념이 추가된다.

handler adapter 는 여러 컨트롤러를 저장하고 사용할 수 있게 하는 역할을 가진다.
handler 는 이전에 사용한 Controller 와 개념이 유사하다.

package hello.servlet.web.frontcontroller.v5;

public interface MyHandlerAdapter {

    boolean supports(Object handler);

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

}

MyHandlerAdapter 인터페이스 코드는 위와 같다. supports 는 핸들러를 사용할 수 있는지 여부를 판단해준다.

handler 메서드는 Controller 를 연결해주는 역할을 한다.

이제 Adapter 를 구현해보자.

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

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) { // ctrl+alt+M - 람다식 메서드 반환
        Map<String, String> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
            .forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
        // paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
        return paraMap;
    }
}

먼저 supports 메서드를 구현한 코드를 보면 instanceof 를 통해 지원하는지 여부를 판단한다.
ControllerV3HandlerAdapter 이므로 V3 버전인지 판단해주는 기능을 한다.

handle 메서드는 handlerControllerV3 로 캐스팅한다. 그리고 paramMap 을 통해 reqeust 의 파라미터를 저장하고 ModelView 를 반환한다.

이제 frontController 를 보자.

package hello.servlet.web.frontcontroller.v5;

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

        // 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) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyHandlerAdapter adapter = getHandlerAdapter(handler);

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

        // 논리적 주소를 물리적 주소로 반환해야한다.
        String viewName = mv.getViewName();

        // 아래 코드를 통해 파일의 경로나 확장자를 유연하게 대처할 수 있다.
        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(); // URI 정보를 받는다.
        return handlerMappingMap.get(requestURI);
    }

    private static MyView viewResolver(String viewName) {
        // 객체를 생성하는것은 service 클래스에 준위가 맞지 않기 때문에 따로 클래스를 뽑아 주는게 좋다.
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

생성자에 두개의 init 메서드가 있다. handlerMappingMap 에게 경로와 컨트롤러를 넣어준다. handlerAdapters 에게는 사용할 컨트롤러 어뎁터 객체를 저장한다.

이로 인해서 getHandlerAdapter 메서드에서는 handlerAdapters 에 저장된 컨트롤러 어뎁터 객체를 반복문으로 하나하나씩 supports 하는지 확인해준다. 그리고 맞는 어뎁터를 반환한다.

그리고 getHandler 메서드를 통해 Object handler 객체에게 사용할 컨트롤러를 반환해준다.

마지막으로 v4 버전도 어뎁터를 만들어 보자.

📌 유연한 컨트롤러2 - v5

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

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) { // ctrl+alt+M - 람다식 메서드 반환
        Map<String, String> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
            .forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
        // paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
        return paraMap;
    }
}

V4 버전에서는 뷰의 이름을 반환한다. 따라서 model 객체를 생성한 뒤에 논리적 주소를 가져오고 ModelView 에게 viewName 을 저장한 뒤에 반환해준다.

앞서 살펴본 FrontControllerServletV4 코드와 유사하다.

📌 정리

지금까지 5개의 버전을 통해서 MVC 모델을 개선해왔다.

프론트 컨트롤러를 도입하고 반복되거나 중복되는 코드를 제거한뒤에 , 어뎁터를 통해 실용성을 추가했다.

지금까지 만든 MVC 패턴은 실제로 Spring MVC 패턴과 유사하다. 다음에는 Spring MVC 패턴에 대해 알아보자.

profile
울산대학교 IT융합학부

0개의 댓글