Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

Kwon's Study Blog !

[Spring] 스프링 MVC 핵심 기술 - MVC 프레임워크 만들기 본문

Spring

[Spring] 스프링 MVC 핵심 기술 - MVC 프레임워크 만들기

순샤인 2022. 5. 3. 19:31

이글은 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 학습 후 
나중에 다시 복습하기 위해 정리한 글입니다.
문제시 비공개로 처리하겠습니다.
 

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

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

www.inflearn.com

 

목차

  1. 프론트 컨트롤러 패턴 소개 
  2. 프론트 컨트롤러 도입 - v1
  3. View 분리 - v2
  4. Model 추가 - v3
  5. 단순하고 실용적인 컨트롤러 - v4
  6. 유연한 컨트롤러1 - v5

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

프론트 컨트롤러 도입 전

프론트 컨트롤러 도입 후

FrontController 패턴 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 입구를 하나로!
  • 공통 처리 가능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

스프링 웹 MVC와 프론트 컨트롤러

스프링 웹 MVC의 핵심도 바로 FrontController (DispatcherServlet)


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

프론트 컨트롤러를 단계적으로 도입해보자.

ControllerV1

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

회원 가입폼, 회원 가입, 회원 목록

public class MemberFormControllerV1 implements ControllerV1 {
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/view/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);
    }
}


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


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

        request.setAttribute("member",member);

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

프론트 컨트롤러

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

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

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

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV1.service");
        String requestURI = request.getRequestURI();

        ControllerV1 controller = controllerMap.get(requestURI);
        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controller.process(request,response);
    }
}

문제점 

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있다. 

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

 

이를 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만들자.


3. View 분리 - v2

MyView 

이 클래스가 JSP 페이지로 렌더링 시켜준다.

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

ControllerV2

public interface ControllerV2 {
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, 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");
    }
}


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

        request.setAttribute("member",member);

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


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

    }
}

프론트 컨트롤러

@WebServlet(name = "frontControllerServletV2",urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
    private Map<String,ControllerV2> controllerMap = new HashMap<>();

    public FrontControllerServletV2(Map<String, ControllerV2> controllerMap) {
        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();

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

문제점

- 서블릿 종속성 

컨트롤러 입장에서 HttpServletRequest, HttpServletResponse 가 꼭 필요할까?

요청 파라미터 정보는 자바의 Map 으로 대신 넘기면, 지금 구조에선 컨트롤러가 서블릿을 기술을 몰라도 동작한다.

 

- 뷰 이름 중복 

컨트롤러에서 return new MyView("/WEB-INF/views/~") 처럼 중복되는 것을 확인할 수 있다.

컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치 이름은 프론트 컨트롤러에서 처리하도록 단순화 하자.

이렇게 해두면 향후 뷰의 폴더 위치가 함꼐 이동해도 프론트 컨트롤러만 고치면된다.

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

4. Model 추가 - v3

ModelView

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

MyView

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

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

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

ControllerV3

public interface ControllerV3 {
    ModelView process(Map<String,String> paramMap);
}

회원 가입폼, 회원 가입, 회원 목록

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


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 modelView = new ModelView("save-result");
        modelView.getModel().put("member",member);
        return modelView;
    }
}


public class MemberListControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();

        ModelView modelView = new ModelView("members");
        modelView.getModel().put("members",members);

        return modelView;
    }
}

프론트 컨트롤러

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
    private Map<String,ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3(Map<String, ControllerV3> controllerMap) {
        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();

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

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

        String viewName = modelView.getViewName();
        MyView myView = viewResolver(viewName);
        myView.render(modelView.getModel(),request,response);


    }

    private Map<String,String> createParamMap(HttpServletRequest request){
        Map<String,String> paramMap = new HashMap<>();

        request.getParameterNames().
                asIterator().forEachRemaining(paramName ->
                        paramMap.put(paramName,request.getParameter(paramName)));
        return paramMap;
    }

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

}

문제점

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

그런데 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에선, 

항상 ModelView 객체를 생성하고 반환해야 하는 부분도 조금은 번거롭다. 

소위 실용성이 없다.


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

기본적인 구조는 V3와 같다. 대신에 컨트롤러가 ModelView를 반환하지 않고, ViewName 만 반환한다.

이번 버전의 ModelView, ViewName은 저번 버전을 그대로 사용하면 된다.

ControllerV4

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

회원 가입폼, 회원 가입, 회원 목록

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


public class MemberSaveControllerV4 implements ControllerV4 {
    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";
    }
}


public class MemberListControllerV4 implements ControllerV4 {
    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";
    }
}

프론트 컨트롤러 

@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
    private Map<String,ControllerV4> controllerMap = new HashMap<>();

    public FrontControllerServletV4(Map<String, ControllerV4> controllerMap) {
        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();

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

        MyView myView = viewResolver(viewName);
        myView.render(model,request,response);
    }

    private Map<String,String> createParamMap(HttpServletRequest request){
        Map<String,String> paramMap = new HashMap<>();
        request.getParameterNames().
                asIterator().forEachRemaining(paramName->
                        paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
    private MyView viewResolver(String viewName){
        return new MyView("/WEB-INF/views" + viewName + ".jsp");
    }
}

문제점

만약 어떤 개발자는 v3 방식으로 개발하고 싶고, 어떤 개발자는 v4 방식으로 개발하고 싶다면 어떻게 해야할까?


6. 유연한 컨트롤러 - v5

어댑터 패턴

지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용 가능하다.

어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자.

  • 핸들러 어댑터 : 중간에 어댑터처럼 다양한 종류의 컨트롤러를 호출해주는 역할
  • 핸들러 : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경.

어댑터 인터페이스

public interface MyHandlerAdapter {
    boolean supports(Object handler);
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}

V3 어댑터

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 modelView = controller.process(paramMap);
        return modelView;
    }

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

        return paramMap;
    }
}

V4 어댑터

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

프론트 컨트롤러

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

        MyView view = viewResolver(mv.getViewName());
        view.render(mv.getModel(),request,response);
    }

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

    private MyHandlerAdapter getHandlerAdapter(Object handler){
        for(MyHandlerAdapter adapter : handlerAdapters){
            if(adapter.supports(handler)){
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter 를 찾을 수 없습니다. handler = " + handler);
    }

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

대략적인 순서 요약

1. 프론트 컨트롤러가 생성되며 생성자에서 핸들러(컨트롤러)들과 핸들러 어댑터 목록을 셋팅한다.

2. url pattern = /front-controller/v5/* 로 들어오면 service 함수로 들어간다.

3. service 함수에서 getHandler로 request 온 uri 에 맞는 핸들러를 찾는다.

4. 찾으면 그 핸들러에 맞는 핸들러 어댑터를 찾는다.

5. 찾으면 adapter의 handle 메서드로 해당 컨트롤러의 process 를 진행하고 ModelView를 반환.

6. 다시 프론트 컨트롤러에서 viewResolver를 호출하고 view를 렌더링한다.


정리

v1 : 프론트 컨트롤러를 도입

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

v2 : View 분류

단순 반복되는 뷰 로직 분리

v3 : Model 추가

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

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

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

v5 : 유연한 컨트롤러

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

 

애노테이션을 지원하는 어댑터를 추가시키면, 

여기에 애노테이션을 사용해서 컨트롤러를 더 편리하게 발전시킬 수 있다.

다형성과 어댑터 덕분에 기존 구조를 유지하면서, 프레임워크의 기능을 확장할 수 있다.

 

 

다음엔 스프링에서의 MVC 프레임워크 구조를 봐보자.