728x90
반응형
SMALL

참고자료:

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 | 김영한 - 인프런

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com

 

이제 스프링 MVC의 핵심 구조를 하나하나 직접 구현해보면서 이해해보자.

핵심은 바로 FrontController.

 

결국 공통으로 처리되어야 할 부분들을 앞에서 모두 처리하고 필요한 컨트롤러만 찾아서 호출해주는 것이다.

기존 코드를 이 FrontController를 도입해서 하나씩 바꿔가보자.

 

우선, ControllerV1 이라는 인터페이스를 만든다. FrontController에서 가장 중요한 것 중 하나는 '다형성'이다.

 

ControllerV1

package org.example.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;
}

 

이 인터페이스를 구현하는 각각의 컨트롤러(Save, Form, List)를 만들자.

 

MemberFormControllerV1

package org.example.servlet.web.frontcontroller.v1.controller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.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/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

 

MemberListControllerV1

package org.example.servlet.web.frontcontroller.v1.controller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.v1.ControllerV1;

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

public class MemberListControllerV1 implements ControllerV1 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

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

        // Model에 보관
        request.setAttribute("members", members);

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

 

MemberSaveControllerV1

package org.example.servlet.web.frontcontroller.v1.controller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.v1.ControllerV1;

import java.io.IOException;

public class MemberSaveControllerV1 implements ControllerV1 {

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

 

이제 이 각각의 컨트롤러의 앞에서 공통 부분을 처리하고 필요한 컨트롤러를 호출해주는 FrontController를 만들면 된다.

FrontControllerServletV1

package org.example.servlet.web.frontcontroller.v1;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import org.example.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import org.example.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;

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 final 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 req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();

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

        controller.process(req, resp);
    }
}

 

이 FrontController는 Servlet이다. 이 서블릿의 URL Pattern은 "/front-controller/v1/*"이다. 이게 의미하는 건 /front-controller/v1/으로 시작하는 모든 URL에 대해 이 서블릿이 처리하겠다는 의미가 된다. 그리고 이 서블릿은 Map을 가진다. 이 Map엔 각 URL에 상응하는 컨트롤러가 담겨있고 이 컨트롤러들은 모두 타입이 ControllerV1이다. 왜냐? 다형성 덕분에 가능하다.

 

그래서 이 Front Controller는 받은 요청의 URI를 통해 어떤 컨트롤러를 호출할지 찾아내서 그 컨트롤러가 구현한 메서드인 process()를 호출한다. 

 

"어? 더 불편해 보이는데요..?"

맞다. 여전히 지금은 각 컨트롤러마다 어떤 뷰를 보여줘야 하는지에 대한 코드가 중복으로 남아있다. 이런 코드들.

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

 

이제 이 부분을 공통으로 처리하게 해보자. 그럼 구조는 이렇게 생기게 된다.

 

바뀌는 부분은 FrontController가 요청으로 들어온 URI를 보고 Controller를 찾는데, 찾고 그 Controller가 가진 process() 메서드를 호출하고 끝나는게 아니라 호출하면 반환하는 MyView라는 객체의 render() 메서드를 호출하는 것까지 FrontController가 하게 된다.

 

위에서도 말했지만, 공통으로 처리될 부분을 앞에서 다 해주는 것이 원래 기대값이다. 그 방향으로 하나씩 나아가는 중인 것.

 

우선 MyView 라는 뷰를 렌더링 하는것을 담당하는 클래스를 만든다.

 

MyView

package org.example.servlet.web.frontcontroller;

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

import java.io.IOException;

public class MyView {
    private final 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 필드를 가지고 있다. 컨트롤러에서 이 객체를 만들 때 viewPath를 넣어주면 이 객체의 render() 메서드는 viewPath를 통해 JSP를 호출한다.

 

이번엔 컨트롤러들의 인터페이스를 만든다.

ControllerV2

package org.example.servlet.web.frontcontroller.v2;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.MyView;

import java.io.IOException;

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

 

이전 V1과 달라지는 것은 반환하는 MyView 객체가 있다는 것.

 

이제 ControllerV2를 구현할 세가지의 컨트롤러(Form, Save, List)를 만든다.

 

MemberFormControllerV2

package org.example.servlet.web.frontcontroller.v2.controller;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.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");
    }
}

 

MemberListControllerV2

package org.example.servlet.web.frontcontroller.v2.controller;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.servlet.web.frontcontroller.v2.ControllerV2;

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

public class MemberListControllerV2 implements ControllerV2 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

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

        // Model에 보관
        request.setAttribute("members", members);
        return new MyView("/WEB-INF/views/members.jsp");
    }
}

 

MemberSaveControllerV2

package org.example.servlet.web.frontcontroller.v2.controller;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.servlet.web.frontcontroller.v2.ControllerV2;

import java.io.IOException;

public class MemberSaveControllerV2 implements ControllerV2 {

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

 

보다시피 이제 뷰에 대한 중복 코드가 컨트롤러에서 사라지고 깔끔하게 정리됐다. 이 작업만으로도 컨트롤러의 코드가 더 깔끔하고 보기 좋아졌다. 이제 이 컨트롤러의 process() 메서드를 호출하는 FrontController를 만들어야한다. 왜냐하면 이제 이것만 호출하고 끝이 아니라 얘가 반환하는 MyView 객체를 가지고 render() 메서드를 호출해줘야 한다.

 

FrontControllerServletV2

package org.example.servlet.web.frontcontroller.v2;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import org.example.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import org.example.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;

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 final 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 req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();

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

        MyView view = controller.process(req, resp);
        view.render(req, resp);
    }
}

 

이제 이 FrontController는 꽤나 어깨가 무거워졌다. URI에 따라 어떤 컨트롤러를 호출할지 정해줘야하고, 컨트롤러가 호출해서 돌려준 MyView 객체로 뷰를 요청에 대한 응답으로 돌려줘야한다. 그래도 한 곳에서 관리하기 때문에 중복이 많이 제거됐다.

 

그렇지만, 여전히 뭔가 아쉽다. 예를 들면 다음 코드.

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

저 경로의 위치가 컨트롤러마다 전부 중복으로 쓰여지고 있다. 그리고 HttpServletRequest, HttpServletResponse가 필요가 없는 컨트롤러도 있다. MemberFormControllerV2를 보면 아예 사용자체가 안되지만 파라미터로 넘겨받고 있다.

 

그리고 다른 컨트롤러도 파라미터 가져오거나, setAttribute() 메서드로 모델에 데이터를 담는 작업 외엔 하는것도 없다. 그러니까 HttpServletResponse는 진짜로 하는게 없다. 그래서, 아예 HttpServletRequest, HttpServletResponse를 사용하지 말아보자.

필요한 파라미터나 모델을 담는것은 서블릿에 종속적이지 않아도 될 것 같다.

 

V3

그래서 ModelView라는 클래스를 만들자. 이 클래스는 모델과 뷰를 동시에 다루는 클래스이다.

어떤 뷰를 보여줄지에 대한 정보와 그 뷰에서 사용할 모델을 동시에 가지고 있는 것이다.

 

ModelView

package org.example.servlet.web.frontcontroller;

import lombok.Getter;
import lombok.Setter;

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

@Getter
@Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

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

어떤 뷰를 보여줘야 하는지 알고 있는 viewName, 그 뷰에서 사용될 데이터를 담은 model이 있다.

이제 이 클래스를 반환타입으로 컨트롤러가 사용하면 된다.

 

ControllerV3

package org.example.servlet.web.frontcontroller.v3;

import org.example.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {

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

이번엔 HttpServletRequest, HttpServletResponse가 필요가 없다. 받는 파라미터는 그저 해당 뷰에서 사용될 파라미터 정보뿐이다. 

 

이 ControllerV3를 구현할 세 가지 컨트롤러(Form, Save, List)를 만들자

 

MemberFormControllerV3

package org.example.servlet.web.frontcontroller.v3.controller;

import org.example.servlet.web.frontcontroller.ModelView;
import org.example.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

package org.example.servlet.web.frontcontroller.v3.controller;

import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberSaveControllerV3 implements ControllerV3 {

    private final 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

package org.example.servlet.web.frontcontroller.v3.controller;

import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.v3.ControllerV3;

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

public class MemberListControllerV3 implements ControllerV3 {

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

 

이제는 각각의 컨트롤러가 서블릿에 대한 종속이 전혀 없다. 보다시피 파라미터로 HttpServletRequest, HttpServletResponse를 받지도 않고 있다. 그리고 ModelView라는 직접 만든 클래스를 사용해서 보여줄 뷰의 이름과 그 뷰에서 사용할 모델을 처리한 후 이 ModelView 객체를 반환한다.

 

그럼 FrontController는 이제 이 반환값을 가지고 공통적으로 또 처리해 줄 것들을 처리하면 된다.

 

FrontControllerServletV3

package org.example.servlet.web.frontcontroller.v3;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import org.example.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import org.example.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 final 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 req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();

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

        Map<String, String> paramMap = createParamMap(req);

        ModelView mv = controller.process(paramMap);

        MyView view = viewResolver(mv);

        view.render(mv.getModel(), req, resp);
    }

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

    private static Map<String, String> createParamMap(HttpServletRequest req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames()
                .asIterator()
                .forEachRemaining(paramName ->
                        paramMap.put(paramName, req.getParameter(paramName))
                );
        return paramMap;
    }
}

 

FrontController는 두 가지 공통작업을 처리한다.

  • 각 컨트롤러가 필요한 파라미터에 대한 작업 (createParamMap())
  • 각 컨트롤러가 보여줄 뷰에 대한 경로 작업 (viewResolver())

그럼 끝인가? 아쉽지만 아니다. 어떤게 남았나면 이제 우리가 직접 만든 모델을 사용하기 때문에 그 모델에 담긴 데이터를 다시 서블릿의 request에 넣어줘야 한다. JSP는 HttpServletRequest 객체인 requestgetAttribute()를 통해서 데이터를 꺼내오기 때문에 꼭 해줘야 한다. 그게 새롭게 만든 MyViewrender()메서드이다.

public void render(Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    model.forEach(req::setAttribute);

    RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
    dispatcher.forward(req, resp);
}

 

이렇게 하면, FrontController가 하는 일이 많아진 대신 각각의 세부 컨트롤러는 하는일이 더더욱 적어졌다. 그리고 FrontController가 중복적인 부분을 혼자 담당하기 때문에 변경이 필요하면 이 부분만 변경하면 된다. 예를 들면 뷰의 경로가

 

"/WEB-INF/views/" + mv.getViewName() + ".jsp" 여기서 "/WEB-INF/jsp/" + mv.getViewName() + ".jsp" 이렇게 변경되더라도 말이다.

 

그러나, 만든 V3 컨트롤러는 잘 설계된 컨트롤러는 맞지만 (서블릿 종속성을 제거하고, 뷰 경로의 중복을 제거하는 등) 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 보면, 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다. 좋은 프레임워크는 아키텍쳐도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 소위 실용성이 있어야 한다.

 

이번에는 V3를 조금 변경해서 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있는 V4 버전을 만들어보자!

 

V4

스프링은 ModelView를 반환하는 컨트롤러를 만들수도 있지만, 그냥 단순 스트링을 반환하고 그 스트링이 뷰의 이름이되는 반환을 하기도 한다. 이 V4는 그것을 똑같이 만들어보고 싶은것이다. 

 

지금 상태에서 크게 바꿀것도 없다. 우선 ControllerV4를 만들자.

 

ControllerV4

package org.example.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {
    /**
     *
     * @param paramMap
     * @param model
     * @return
     */
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 

참고로, 저 메서드레벨의 주석 단축키가 굉장히 편한데 그냥 `/**` 입력하고 엔터만 치면 자동으로 저렇게 써준다.

 

이제는 V4는 한가지 파라미터를 더 받는다. model이다. 그래서 ModelView를 컨트롤러가 모두 반환해야 하는 불편함을 없애는 것.

이제 Form, List, Save 컨트롤러를 만들어보자.

 

MemberFormControllerV4

package org.example.servlet.web.frontcontroller.v4.controller;

import org.example.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";
    }
}

이제 process() 메서드는 단순 스트링만을 반환할 수 있다. 귀찮게 ModelViewnew로 생성해서 반환할 필요가 사라졌다.

그리고 저 반환하는 스트링은 바로 뷰의 이름이 된다. 

MemberListControllerV4

package org.example.servlet.web.frontcontroller.v4.controller;

import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.v3.ControllerV3;
import org.example.servlet.web.frontcontroller.v4.ControllerV4;

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

public class MemberListControllerV4 implements ControllerV4 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    /**
     *
     * @param paramMap
     * @param model
     * @return
     */
    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        List<Member> members = memberRepository.findAll();

        model.put("members", members);

        return "members";
    }
}

모든 멤버들을 보여주는 MemberListControllerV4. 이것 역시 그냥 단순히 스트링을 반환한다. 마찬가지로 반환하는 문자열은 뷰의 이름이 된다.

MemberSaveControllerV4

package org.example.servlet.web.frontcontroller.v4.controller;

import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import org.example.servlet.web.frontcontroller.v4.ControllerV4;

import java.util.Map;

public class MemberSaveControllerV4 implements ControllerV4 {

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

이번엔 멤버를 저장하는 컨트롤러이다. 마찬가지다. 변경되는 지점은 받는 파라미터에 model이 추가되고, 반환 타입이 String.

이렇게 컨트롤러들을 전부 변경했으니 FrontController도 변경해보자.

 

FrontControllerServletV4

package org.example.servlet.web.frontcontroller.v4;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import org.example.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import org.example.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 final 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());
    }

    /**
     *
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();

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

        Map<String, String> paramMap = createParamMap(req);
        Map<String, Object> model = new HashMap<>();

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

        MyView view = viewResolver(viewName);

        view.render(model, req, resp);
    }

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

    private static Map<String, String> createParamMap(HttpServletRequest req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames()
                .asIterator()
                .forEachRemaining(paramName ->
                        paramMap.put(paramName, req.getParameter(paramName))
                );
        return paramMap;
    }
}

여기서 변경되는 부분은 service()에서 적절한 컨트롤러의 process()를 실행할 때 model을 추가적으로 넘겨주는 것과 받는 반환타입이 바로 viewResolver()의 파라미터로 들어간다는 점이다. 그리고 view.render()의 첫번째 파라미터가 model이 되면 된다.

 

이러면 모든게 기존과 동일하게 동작한다. 어떤 부분에서 더 유연해졌냐?

  • 실제 컨트롤러들(FrontController 말고)이 굳이 ModelView를 매번 새로운 인스턴스로 만들어내지 않아도 된다. 특히 모델이 아예 필요없고 뷰의 논리 이름만을 위해서 만드는 MemberFormController의 경우 정말 비효율적인 방식이었는데 이를 깔끔하게 해결해준다.

 

이 방식이 바로 스프링이 컨트롤러를 만들때 ModelView를 반환해도 가능하고 단순 String을 반환해도 상관없는 이유이다. 

"어?! 근데 지금 코드는 단순 String만 반환 가능할 거 같은데요?" 맞다. 왜냐하면, 이 FrontControllerV4는 컨트롤러를 확정짓기 위해 사용되는 controllerMapValueControllerV4로 한정되어 있다. 그러나 스프링은 두 가지 경우 모두 지원한다. 즉, 더 유연하다는 소리고 그 방법을 V5에서 알아보자!

 

V5

지금까지의 구조는 다음과 같다.

위 구조를 흐름대로 설명하면 다음과 같은 흐름이 발생한다.

  • 사용자로부터 요청이 들어온다.
  • 요청을 최초에 FrontControllerV4가 받는다.
  • 요청 URL에 따라 처리 가능한 컨트롤러를 FrontControllerV4는 찾고 그 컨트롤러를 호출한다.
  • 해당 컨트롤러에서 필요한 수행 작업을 모두 마친 후 보여줄 화면에 대한 뷰 이름을 가진 반환값을 FrontControllerV4에게 돌려준다.
  • 받은 뷰 이름을 전체 이름으로 변경해주는 viewResolver()FrontControllerV4가 호출한다.
  • 호출해서 받은 전체 뷰 경로를 가지고 MyView 객체의 render()를 호출해서 사용자에게 최종 화면을 보여준다.

여기서 개선될 부분은 개발자가 "나는 V4 형태 말고 V3 형태로 컨트롤러를 만들고 싶어!"라고 할 때 그러지 못한다는 점이다. 이 점을 가능하게 변경해보자. 

 

우선, MyHandlerAdapter라는 인터페이스 하나가 필요하다.

MyHandlerAdapter

package org.example.servlet.web.frontcontroller.v5;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.ModelView;

import java.io.IOException;

public interface MyHandlerAdapter {
    boolean supports(Object handler);

    ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler)
            throws ServletException, IOException;
}

 

이 인터페이스는 구현체를 봐야 좀 더 이해가 명확하게 되니까 구현체도 바로 만들자.

ControllerV3HandlerAdapter

package org.example.servlet.web.frontcontroller.v5.adapter;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.v3.ControllerV3;
import org.example.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) {
        return (handler instanceof ControllerV3);
    }

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

        Map<String, String> paramMap = createParamMap(req);

        return controller.process(paramMap);
    }

    private static Map<String, String> createParamMap(HttpServletRequest req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames()
                .asIterator()
                .forEachRemaining(paramName ->
                        paramMap.put(paramName, req.getParameter(paramName))
                );
        return paramMap;
    }
}

보면, ControllerV3HandlerAdapter라는 구현체가 있다. 이 구현체가 하는 역할은 다음과 같다.

  • supports()에서 들어온 파라미터가 ControllerV3 타입인지를 판단한다. 만약 그렇다면, 참을 반환한다.
  • handle()supports()가 참을 반환했을 때 유효한 메서드이다. 들어온 handlerControllerV3 타입이라면 이 handle()ControllerV3가 했던 동작을 그대로 할 뿐이다.

이 둘만 가지고는 이해가 제대로 되지 않는다. FrontControllerV5를 만들어보자.

FrontControllerServletV5

package org.example.servlet.web.frontcontroller.v5;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import org.example.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import org.example.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import org.example.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 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 req, HttpServletResponse resp) throws ServletException, IOException {
        Object handler = getHandler(req);
        if (handler == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyHandlerAdapter handlerAdapter = getHandlerAdapter(handler);
        ModelView mv = handlerAdapter.handle(req, resp, handler);

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

        view.render(mv.getModel(), req, resp);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter handlerAdapter : handlerAdapters) {
            if (handlerAdapter.supports(handler)) {
                return handlerAdapter;
            }
        }
        throw new IllegalArgumentException("handler not found for class " + handler.getClass().getName());
    }

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

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
  • handlerMappingMap URL에 따라 처리하는 컨트롤러를 모두 저장해 놓는 Map이다.
  • handlerAdapters는 모든 MyHandlerAdapter 타입의 객체를 담는 List이다.
  • 생성자에서 두 가지 메서드를 호출한다. initHandlerMappingMap(), initHandlerAdapters().
  • initHandlerMappingMap()은 요청 URL에 따라 처리를 할 수 있는 컨트롤러를 모두 Map에 추가해주는 메서드이다. 위 코드를 보면 `/front-controller/v5/v3/members`로 들어온 URL은 MemberListControllerV3로 Key/Value를 가진다.
  • initHandlerAdapters()MyHandlerAdapter 타입의 모든 인스턴스를 추가한다. 위 코드를 보면 ControllerV3HandlerAdapter를 추가했음을 알 수 있다.

이제 여기서부터 실제 흐름이다. 어떤 흐름을 통해 동작하는지 이해해보자.

  • 첫번째로 FrontControllerV5로 모든 사용자의 요청이 들어오게 된다. 
  • 사용자의 요청에 따라 첫번째로 할 일은 요청 URL과 매핑해둔 컨트롤러(핸들러)를 찾는다. -> `getHandler()`
  • 만약 핸들러를 찾지 못했다면, NOT_FOUND 에러를 내보낸다.
  • 핸들러를 찾았다면, 그 핸들러를 통해 핸들러 어답터를 찾는다. 즉, 찾은 핸들러가 만약 MemberListControllerV3였다면, 이 핸들러를 통해 handlerAdapters에서 쭉 루프를 돌면서 핸들러 어답터를 찾는다. 어떻게 찾을까? handlerAdapters에는 모든 MyHandlerAdapter를 구현한 구현체가 들어있게 된다. 각 구현체는 supports()를 구현해야 하는데 이 메서드는 들어온 파라미터가 ControllerV3, ControllerV4 타입인지를 체크한다. 찾았다면 해당 핸들러어답터를 가져오고 찾지 못했다면 에러를 던진다.
  • 가져온 핸들러 어답터의 handle()을 호출한다. 이 handle()은 V3 버전인지 V4 버전인지에 따라 처리하는 로직을 구분지어 각 버전에 맞게 컨트롤러가 처리하는 로직이 담겨있다.

 

이렇게 어떤 버전의 컨트롤러라도 처리할 수 있는 FrontControllerV5가 만들어지게 된다. 스프링도 이렇게 구현해 두었다. 그래서 스프링도 컨트롤러의 메서드 중 반환타입이 단순 문자열인 메서드가 존재하고 ModelAndView 타입의 메서드가 존재해도 상관이 없는 것이다.

 

그럼 V3 관련 컨트롤러를 처리해 놨으니 V4도 어댑터를 만들어보자.

ControllerV4HandlerAdapter

package org.example.servlet.web.frontcontroller.v5.adapter;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.v4.ControllerV4;
import org.example.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) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler)
            throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;

        Map<String, String> paramMap = createParamMap(req);
        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 req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames()
                .asIterator()
                .forEachRemaining(paramName ->
                        paramMap.put(paramName, req.getParameter(paramName))
                );
        return paramMap;
    }
}

마찬가지로 이 V4 핸들러 어댑터는 MyHandlerAdapter를 구현한다. 그리고 구현 내용이 V3가 아닌 V4일 뿐이다.

 

그리고 FrontControllerV5를 보자.

FrontControllerServletV5

package org.example.servlet.web.frontcontroller.v5;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.web.frontcontroller.ModelView;
import org.example.servlet.web.frontcontroller.MyView;
import org.example.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import org.example.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import org.example.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import org.example.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import org.example.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import org.example.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import org.example.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
import org.example.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 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());
        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 req, HttpServletResponse resp) throws ServletException, IOException {
        Object handler = getHandler(req);
        if (handler == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyHandlerAdapter handlerAdapter = getHandlerAdapter(handler);
        ModelView mv = handlerAdapter.handle(req, resp, handler);

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

        view.render(mv.getModel(), req, resp);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter handlerAdapter : handlerAdapters) {
            if (handlerAdapter.supports(handler)) {
                return handlerAdapter;
            }
        }
        throw new IllegalArgumentException("handler not found for class " + handler.getClass().getName());
    }

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

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

변경되는 부분은 handlerAdaptersV4HandlerAdapter가 추가되고, handlerMappingMap에 V4관련 컨트롤러를 추가한 것뿐이다. 그 외에는 아무런 변경사항이 없다. 심지어 이 handlerAdaptershandlerMappingMap도 밖에서 주입받게 해 놓으면 아예 이 FrontCotrollerV5는 변경 사항이 아예 없어진다. 이게 변경에는 닫혀있고 확장에는 열려있는 OCP 원칙이다.

 

스프링도 이와 유사한 (거의 똑같다) 구조를 가지고 있고 이렇게 만들어 놓다가 애노테이션 기반의 컨트롤러가 대세가 되면서 애노테이션 기반의 컨트롤러를 처리할 수 있는 어댑터를 하나 만들어서 이 핸들러 어댑터에 추가만 해줄뿐이다. 그러니까 확장이 너무 유연해지고 간결해지는 것이다. 

 

결론

그레서 최종 V5의 모습은 이와 같다.

  • 중간에 핸들러 어댑터라는게 추가됐다. 이 핸들러 어댑터 덕분에 여러 버전의 컨트롤러를 만들어도 아무런 문제없이 해당 버전에 맞는 컨트롤러 처리를 할 수 있게 됐다.
  • 핸들러는 그저 컨트롤러의 다른말 일뿐이다.

이 구조가 바로 Spring MVC 구조이다. 그리고 잠깐 위에서 말했지만 애노테이션 기반의 컨트롤러가 대세가 되면서 이 애노테이션 기반의 컨트롤러를 처리할 수 있는 핸들러 어댑터 하나만 만들어주면 되는 식으로 확장이 용이하다고 했는데 스프링에서 이 핸들러 어댑터 이름이 바로 "RequestMappingHandlerAdapter"이다. 느낌이 바로 오지 않는가? V5 구조에서 만든 핸들러 어댑터 이름은 ControllerV4HandlerAdapter, ControllerV5HandlerAdapter였다. 이제 @RequestMapping("/hello") 이런 애노테이션 기반의 컨트롤러를 처리할 수 있는 핸들러 어댑터가 필요하니까 핸들러 어댑터를 만들었는데 그 이름이 저것인거다. 

728x90
반응형
LIST

+ Recent posts