본문 바로가기
Spring/MVC 1편

인프런 MVC 1 강의 정리 #4

by 예린lynn 2024. 1. 1.
728x90

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

- 프론트 컨트롤러 도입 전

도입 전에는 공통 로직을 각각 생성해야 했다.

 

- 프론트 컨트롤러 도입 후

프론트 컨트롤러라는 서블릿을 도입하면 공통 로직을 하나 생성하고, 다른 컨트롤러들을 호출한다.

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

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

기존 코드를 최대한 유지하면서, 점진적으로 프론트 컨트롤러를 도입해보자.

 

- v1 구조

프론트 컨트롤러는 매핑 정보를 통해 어떤 컨트롤러를 호출해야 하는지에 대한 정보를 받는다. 그 후 해당 컨트롤러를 호출하고, 컨트롤러는 JSP를 foward 한다.

 

1) ControllerV1

먼저 컨트롤러 인터페이스를 만들어보자.  main/hello/servlet/web에 frontcontroller 패키지를 생성하고, 그 안에 v1 패키지를 생성한 후, 그 안에 ControllerV1 인터페이스 클래스를 만든다.

 

서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 각 컨트롤러들은 이 인터페이스를 구현하게 되는데, 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.

import java.io.IOException;
import java.rmi.server.ServerCloneException;

public interface ControllerV1 {

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

}

 

다음으로 이 인터페이스를 구현한 컨트롤러를 만들어야 한다. 

 

2) 회원 등록 컨트롤러

main/hello/servlet/web/fronrcontroller/v1에 controller 패키지를 생성하고, 그 안에 MemberFormController 클래스를 생성한다. 인터페이스로 ControllerV1을 구현하고, jsp를 호출하는 경로를 나타내는 코드를 작성한다.

import java.io.IOException;
import java.rmi.server.ServerCloneException;

public class MemberFormControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServerCloneException, IOException {

        // 경로를 따라 서블릿에서 jsp 호출
        String viewPath="/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher=request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);

    }

 

2) 회원 저장 컨트롤러

main/hello/servlet/web/frontcontroller/v1/controller에 MemberSaveControllerV1 클래스를 생성한다.

ControllerV1을 implements 해야 한다.

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;

import hello.servlet.web.frontcontroller.v1.ControllerV1;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.rmi.server.ServerCloneException;

public class MemberSaveControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServerCloneException, IOException, ServletException {

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

 

3) 회원 목록 컨트롤러

main/hello/servlet/web/frontcontroller/v1/controller에 MemberListControllerV1 클래스를 생성한다.

ControllerV1을 implements 해야 한다.

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

import java.io.IOException;
import java.rmi.server.ServerCloneException;
import java.util.List;

public class MemberListControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository=MemberRepository.getInstance();
    
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServerCloneException, IOException, ServletException {

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

 

4) 프론트 컨트롤러

main/hello/servlet/web/frontcontroller/v1에 FrontControllerServletV1 클래스를 생성한다.

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 jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;

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() {
        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());
    }
    
    @SneakyThrows
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       
        System.out.println("FrontControllerServletV1.service");
        
        // URI 요청하기
        String requestURI = request.getRequestURI();
        ControllerV1 controller = controllerMap.get(requestURI);
        
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        controller.process(request,response);
    }
}

 

-urlPatterns

  • urlPatterns = "/front-controller/v1/*" : /front-controller/v1 를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
  • ex) /front-controller/v1 , /front-controller/v1/a , /front-controller/v1/a/b

-controllerMap

  • key: 매핑 URL
  • value: 호출될 컨트롤러

-service()

  • 먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap에서 찾는다.
  • 만약 없다면 404(SC_NOT_FOUND) 상태 코드를 반환한다.
  • 컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다.

- JSP

  • JSP는 이전 MVC에서 사용했던 것을 그대로 사용한다

 

 3 . View 분리 - v2

모든 컨트롤러의 코드를 살펴보면, 뷰로 이동하는 아래 코드가 중복된다.

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

 

이와 같은 중복 코드를 해결하기 위해 별도로 뷰를 처리하는 객체를 만들 수 있다.

 

- V2 구조 

 

1) MyView

main/java/hello/servlet/web/frontcontroller에 MyView 클래스를 생성한다.

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

 

2) ControllerV2

jaav/hello/servlet/web/frontcontroller에 v2 패키지를 생성하고, 그 안에 ControllerV2 인터페이스를 생성한다.

이때 MyView를 반환하도록 코드를 작성한다.

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

import java.io.IOException;
import java.rmi.server.ServerCloneException;

public interface ControllerV2 {

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

 

3) 회원 등록 폼

hello/servlet/web/frontcontroller/v2에 controller 패키지를 생성하고, 그 안에 MemberFormControllerV2 클래스를 생성한다.

ControllerV2를 implements하고, MyView를 리턴한다.

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.rmi.server.ServerCloneException;

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

 

4) 회원 저장

hello/servlet/web/frontcontroller/v2/controller에 MemberSaveControllerV2 클래스를 생성한다.

회원 등록 폼과 동일하게 MyView를 리턴해준다.

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 jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.rmi.server.ServerCloneException;

public class MemberSaveControllerV2 implements ControllerV2 {

    private MemberRepository memberRepository=MemberRepository.getInstance();
    
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServerCloneException, IOException, ServletException {
        
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        System.out.println("member = " + member);
        memberRepository.save(member);

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

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

 

5) 회원 목록

hello/servlet/web/frontcontroller/v2/controller에 MemberListControllerV2 클래스를 생성한다.

회원 등록 폼과 동일하게 MyView를 리턴해준다.

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 jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.rmi.server.ServerCloneException;
import java.util.List;

public class MemberListControllerV2 implements ControllerV2 {

    private MemberRepository memberRepository=MemberRepository.getInstance();
    
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServerCloneException, IOException, ServletException {

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

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

 

6) 프론트 컨트롤러

hello/servlet/web/frontcontroller/v2에 FrontControllerServletV2 클래스를 생성한다.

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
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 lombok.SneakyThrows;

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

    }

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

 

ControllerV2의 반환 타입이 MyView 이므로 프론트 컨트롤러는 컨트롤러의 호출 결과로 MyView 를 반환 받는다. 그리고 view.render() 를 호출하면 forward 로직을 수행해서 JSP가 실행된다. 프론트 컨트롤러의 도입으로 MyView 객체의 render() 를 호출하는 부분을 모두 일관되게 처리할 수 있다. 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 된다.

 

참고로 render() 코드는 다음과 같이 jsp가 중복적으로 호출되는 부분을 나타낸다.

public void render(HttpServletRequest request, HttpServletResponse response) 
throws ServletException, IOException {
 RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
 dispatcher.forward(request, response);
}

 

 

4 . 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

- v3 구조

지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다. 그리고 Model도 request.setAttribute() 를 통해 데이터를 저장하고 뷰에 전달했다. 그러나 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체를 만들어 더 간결하게 나타낼 수 있다.

 

1) ModelView

hello/servlet/web/frontcontroller에 ModelView 클래스를 생성한다.

뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있다. model은 단순히 map으로 되어 있으므로 컨트롤러에서 뷰에 필요한 데이터를 key, value로 넣어주면 된다.

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

 

2) ControllerV3

hello/servlet/web/frontcontroller에 v3 패키지를 생성하고, 그 안에 ControllerV3 인터페이스를 생성한다.

import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;

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

 

3) 회원 등록 폼

hello/servlet/web/frontcontroller/v3에 controller 패키지를 생성하고, 그 안에 MemberFormControllerV3 클래스를 생성한다.

ModelView를 리턴하고, view의 전체 path가 아니라 논리적 이름(new-form)을 넣어야 한다.

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

 

4) 회원 저장

hello/servlet/web/frontcontroller/v3/controller에 MemberSaveControllerV3 클래스를 생성한다.

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

 

- ModelView mv = new ModelView("save-result");

  • "save-result"라는 뷰 이름을 가지고 ModelView 객체를 생성. 이 뷰는 회원 정보 저장 결과를 보여주는 페이지로 이동하는 데 사용된다.

 

- mv.getModel().put("member", member);

  • mv 객체의 모델에 회원 정보를 담은 member 객체를 "member" 키로 추가. 이를 통해 뷰에서 회원 정보를 활용 가능

 

5) 회원 목록

hello/servlet/web/frontcontroller/v3/controller에 MemberListControllerV3 클래스를 생성한다.

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;

    }
}

 

6) 프론트 컨트롤러

hello/servlet/web/frontcontroller/v3에 FrontControllerServletV3 클래스를 생성한다.

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 jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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() {
        controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV3());
        controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV3());
        controllerMap.put("/front-controller/v2/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 mv=controller.process(paramMap);

        String viewName= mv.getViewName(); // 논리 이름
        MyView view = viewResolver(viewName);

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

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

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

cf) Extract/Introduce -> Extract Method : Ctrl + Alt + M

 

- MyView view = new MyView("/WEB-ING/views/" + viewName + ".jsp");

  • 주어진 논리적인 뷰 이름(viewName)을 기반으로 실제 JSP 파일의 경로를 생성
  • ex) "save-result"라는 논리적인 뷰 이름이 주어졌을 때, 해당 뷰 파일의 경로는 "/WEB-ING/views/save-result.jsp"

- request.getParameterNames()

  •  현재 요청에 포함된 모든 파라미터의 이름을 Enumeration<String> 형태로 반환

- asIterator()

  • Enumeration을 Iterator로 변환

 - forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))):

  • Iterator의 forEachRemaining() 메서드를 사용하여 Iterator를 순회하면서 각 파라미터의 이름을 가져와서 request.getParameter(paramName)을 통해 해당 파라미터의 값을 얻음
  • 그리고 해당 파라미터 이름과 값을 paramMap에 저장. 이때, paramMap은 파라미터 이름을 키(key)로, 파라미터 값(value)을 값으로 저장하는 HashMap이다.
  • 따라서 파라미터 이름과 값을 paramMap에 저장한다.

 

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

v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다. 그러나항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다. 이 부분을 개선해서 더 단순한 v4를 설계해보도록 하자.

 

- v4 구조

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

 

1) ControllerV4

hello/servlet/web/frontcontroller에 v4 패키지를 생성하고, 그 안에 ControllerV4 인터페이스를 생성한다.

인터페이스에 ModelView가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고, 결과로 뷰의 이름만 반환해주면 된다.

import java.util.Map;

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

 

2) 회원 등록 폼

hello/servlet/web/frontcontroller/v4/controller에 MemberFormControllerV4 클래스를 생성한다.

단순하게 new-form 이라는 뷰의 논리 이름만 반환하도록 코드를 작성한다.

import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.Map;

public class MemberFormControllerV4 implements ControllerV4 {

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

 

3) 회원 저장

hello/servlet/web/frontcontroller/v4/controller에 MemberSaveControllerV4 클래스를 생성한다.

모델이 파라미터로 전달되기 때문에, model.put("member", member) 코드를 작성해서 모델을 직접 생성하지 않아도 된다.

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

 

4) 회원 목록

hello/servlet/web/frontcontroller/v4/controller에 MemberListControllerV4 클래스를 생성한다.

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

 

5) 프론트 컨트롤러

hello/servlet/web/frontcontroller/v4에 FrontControllerServletV4 클래스를 생성한다.

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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

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

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

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

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        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 view = viewResolver(viewName);
        view.render(model,request, response);
    }

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

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

 

- Map model = new HashMap<>();

  • 모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담긴다.

 

6 . 유연한 컨트롤러1 - v5

지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다. 예를 들어 ControllerV3 , ControllerV4 는 완전히 다른 인터페이스이므로 호환이 불가능하다. 이때 사용하는 것이 바로 어댑터이다.

 

- v5 구조

  • 핸들러 어댑터: 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다. 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
  • 핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경한 것이다. 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.

1) 핸들러 어댑터

hello/servlet/web/frontcontroller에 v5 패키지를 생성하고, 그 안에 MyHandlerAdapter 인터페이스를 생성한다.

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

import java.io.IOException;

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

 

- boolean supports(Object handler)

  • handler는 컨트롤러를 말한다. 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드다.
  • 처리 가능하면 true, 처리 불가능하면 false 반환

 

- ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)

  • 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다.
  • 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
  • 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출 된다.

2) V3 Controller 어뎁터

hello/servlet/web/frontcontroller/v5에 adapter 패키지를 생성하고, 그 안에 ControllerV3HandlerAdapter 클래스를 생성한다. 

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 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;
    }

}

 

 

- public boolean supports(Object handler)

  • 해당 어댑터가 지원하는지 여부를 판단하는 메서드
  • handler 객체가 ControllerV3 인터페이스를 구현했는지 확인
  • supports() 메서드는 ControllerV3를 구현한 핸들러 객체를 지원하는지 여부를 판단하여 true 또는 false를 반환

- public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)

  • 실제 요청을 처리하는 메서드
  • handle() 메서드는 HttpServletRequest, HttpServletResponse, 그리고 핸들러(Handler)를 인자로 받는다
  • handler 객체를 ControllerV3로 캐스팅하여 해당 컨트롤러의 process() 메서드를 호출하여 ModelView를 반환
  • createParamMap(request)를 통해 HTTP 요청으로부터 파라미터를 추출하여 컨트롤러에 전달

- private Map<String, String> createParamMap(HttpServletRequest request)

  • HTTP 요청으로부터 파라미터를 추출하여 Map<String, String> 형태로 반환

 

3) 프론트 컨트롤러

hello/servlet/web/frontcontroller/v5에 FrontControllerServletV5 클래스를 생성한다.

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

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

    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 initHanderAdapters() {
        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();
        MyView view = new MyView("/WEB-INF/views/"+viewName+".jsp");

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

        }
    }

 

- 생성자

생성자는 핸들러 매핑과 어댑터를 초기화(등록)한다.

public FrontControllerServletV5() {
 initHandlerMappingMap(); //핸들러 매핑 초기화
 initHandlerAdapters(); //어댑터 초기화
}

 

- 매핑 정보

매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변경한다.

private final Map<String, Object> handlerMappingMap = new HashMap<>();

 

- 핸들러 매핑

핸들러 매핑 정보인 handlerMappingMap 에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환한다.

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

 

- 어댑터 조회

handler 를 처리할 수 있는 어댑터를 adapter.supports(handler) 를 통해서 찾는다. handler가 ControllerV3 인터페이스를 구현했다면, ControllerV3HandlerAdapter 객체가 반환된다.

for (MyHandlerAdapter adapter : handlerAdapters) {
 if (adapter.supports(handler)) {
 return adapter;
 }
}

 

- 어댑터 호출

어댑터의 handle(request, response, handler) 메서드를 통해 실제 어댑터가 호출된다. 어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환한다.

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

 

 

 7 . 유연한 컨트롤러 2 - v5

컨트롤러 V4를 호출하는 핸들러 어댑터를 생성해보자. 

 

1) 프론트 컨트롤러에 V4 기능 추가

hello/servlet/web/frontcontroller/v5/FrontControllerServletV5에 V4 기능을 추가하자.

다음과 같이 코드를 추가해서 수정하도록 한다.

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 initHanderAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

 

2) v4 Controller 어댑터

hello/servlet/web/frontcontroller/v5/adapter에 ControllerV4HandlerAdapter 클래스를 생성한다.

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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

 

- 어댑터

handler 가 ControllerV4 인 경우에만 처리하는 어댑터이다.

public boolean supports(Object handler) {
 return (handler instanceof ControllerV4);
}

 

- 실행 로직

handler를 ControllerV4로 케스팅 하고, paramMap, model을 만들어서 해당 컨트롤러를 호출한다. 그리고 viewName을 반환 받는다.

ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);

 

- 어댑터 변환

어댑터가 호출하는 ControllerV4 는 뷰의 이름을 반환한다. 그런데 어댑터는 viewName이 아니라 ModelView 를 만들어서 반환해야 한다.  어댑터는 viewName을 ModelView로 만들어서 형식을 맞추어 반환한다.

ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;

 

 

8 . 정리

  • v1: 프론트 컨트롤러를 도입, 기존 구조를 최대한 유지하면서 프론트 컨트롤러를 도입
  • v2: View 분류, 단순 반복 되는 뷰 로직 분리
  • v3: Model 추가 서블릿 종속성 제거, 뷰 이름 중복 제거
  • v4: 단순하고 실용적인 컨트롤러 v3와 거의 비슷, 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공
  • v5: 유연한 컨트롤러 어댑터 도입, 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계
728x90

'Spring > MVC 1편' 카테고리의 다른 글

인프런 MVC 1 강의 정리 #6  (0) 2024.01.09
인프런 MVC 1 강의 정리 #5  (1) 2024.01.08
인프런 MVC 1 강의 정리 #3  (1) 2023.12.31
인프런 MVC 1 강의 정리 #2  (0) 2023.12.27
인프런 MVC 1 강의 정리 #1  (1) 2023.12.27