Spring

[인프런/스프링 MVC 1편] 4. MVC 프레임워크 직접 만들어보기

주니어주니 2023. 5. 1. 20:51

 

 

 

1. Front Controller 

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 공통 처리 기능
  • 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
  • 스프링 웹 MVC의 핵심 (DispatcherServlet이 FrontController 패턴으로 구현되어 있음)

 

 

- 프론트 컨트롤러 도입 전 (각 컨트롤러에서 매번 공통 기능 처리)

 

 

 

- 프론트 컨트롤러 도입 후 (프론트 컨트롤러에서 공통 기능 처리)

 

 

 


 

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

 

V1 구조

 

 

 

1) ControllerV1 (인터페이스)

  • 서블릿과 비슷한 모양의 컨트롤러 인터페이스
  • 각 컨트롤러들은 이 인터페이스를 구현 
  • 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성 유지

 

package hello.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;

}

 

 

2) 각 컨트롤러 (ControllerV1 인터페이스 구현) 

  • 내부 로직은 각자의 기존 서블릿과 동일

(1) MemberFormControllerV1 - 회원등록 컨트롤러 

 

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

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;

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

 

 

(2) MemberSaveControllerV1 - 회원저장 컨트롤러 

 

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

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

import java.io.IOException;

public class MemberSaveControllerV1 implements ControllerV1 {

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

 

 

(3) MemberListControllerV1 - 회원목록 컨트롤러 

 

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

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

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

public class MemberListcontrollerV1 implements ControllerV1 {

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

 

 

 

3) FrontControllerServletV1 - 프론트 컨트롤러 

  • urlPatterns
    • /front-controller/v1/* : /front-controller/v1/*를 포함한 모든 하위 요청을 이 서블릿에서 받음
  • controllerMap
    • key : 매핑 URL
    • value : 호출될 컨트롤러
      (이 URL을 요청하면 이 컨트롤러 실행)
  • service() 
    • requestURI를 조회해서 해당 URI와 매핑되어 있는 컨트롤러를 controllerMap에서 찾음 
    • 없으면 404(NOT FOUND) 상태코드 반환
    • 컨트롤러를 찾고 controller.process(request, response)를 호출해서 해당 컨트롤러 실행 
  • JSP
    • 매핑 URL은 요청 URL이고, 사용되는 JSP는 각 컨트롤러에서 설정한 JSP 경로 (이전 MVC에서 사용했던 것)
  • 실행
    • 등록 : http://localhost:8080/front-controller/v1/members/new-form
    • 저장 : http://localhost:8080/front-controller/v1/members/save
      등록 jsp에서 form의 action 경로를 상대경로로 저장 ("save") -> 현재 URL이 속한 계층 경로 + save로 이동 
    • 목록 : http://localhost:8080/front-controller/v1/members

 

package hello.servlet.web.frontcontroller.v1;

import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListcontrollerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
import 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 = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

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

    // 컨트롤러 매핑
    public FrontControllerServletV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerMap.put("/front-controller/v1/members", new MemberListcontrollerV1());
    }

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

        // 현재 URI로 FrontControllerServletV1에 담겨있는 컨트롤러 조회
        String requestURI = request.getRequestURI();
        // 다형성 (ControllerV1를 구현한 각 컨트롤러)
        ControllerV1 controller = controllerMap.get(requestURI);

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

        // 해당 컨트롤러 실행
        controller.process(request, response);

    }
}

 

 


 

3. View 분리 - v2

 

각 컨트롤러에서 뷰로 이동할 때 코드가 중복됨 -> 뷰를 별도로 처리하는 객체 생성

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

 

 

V2 구조

 

 

- 각 컨트롤러는 viewPath를 갖고있는 MyView 객체를 반환하기만 함 (어떤 기능 실행 X)

- 프론트 컨트롤러에서 이 반환받은 MyView객체의 render() 메소드 실행하여 뷰로 forward 시킴

 

 

1) MyView - 뷰 객체

 

package hello.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 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 (인터페이스) 

 

package hello.servlet.web.frontcontroller.v2;

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

import java.io.IOException;

public interface ControllerV2 {

    // MyView 객체 반환!
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;

}

 

 

3) 각 컨트롤러 (인터페이스 구현)

 

(1) MemberFormControllerV2 - 회원등록 컨트롤러

 

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

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;

public class MemberFormControllerV2 implements ControllerV2 {

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

        // 지저분했던 ViewPath 설정 없이 MyView 생성자에 ViewPath 작성 후 반환
        return new MyView("/WEB-INF/views/new-form.jsp");

    }
}

 

 

(2) MemberSaveControllerV2 - 회원저장 컨트롤러

 

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

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

import java.io.IOException;

public class MemberSaveControllerV2 implements ControllerV2 {

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

 

 

(3) MemberListControllerV2 - 회원목록 컨트롤러

 

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

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

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

public class MemberListControllerV2 implements ControllerV2 {

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

 

 

4) FrontControllerServletV2 - 프론트 컨트롤러 

  • 각 컨트롤러의 반환타입이 MyView이므로 컨트롤러 호출 결과로 MyView를 반환받음

 

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
import 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 = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

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

    // 컨트롤러 매핑
    public FrontControllerServletV2() {
        controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 현재 URI로 FrontControllerServletV2에 담겨있는 컨트롤러 조회
        String requestURI = request.getRequestURI();

        // 다형성 (ControllerV2를 구현한 각 컨트롤러)
        ControllerV2 controller = controllerMap.get(requestURI);

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

        // 해당 컨트롤러 실행 (각 process()의 반환 결과는 MyView("viewPath"))
        MyView view = controller.process(request, response);
        // 뷰로 렌더링(이동)
        view.render(request, response);
    }
}

 

  • view.render()를 실행하면 MyView객체의 forward()를 수행해서 JSP 실행

 

 

  • 프론트 컨트롤러의 도입으로 MyView 객체의 render()를 호출하는 부분을 모두 일관되게 처리
  • 각 컨트롤러는 MyView 객체를 생성만 해서 반환하면 됨
  • 실행
    • 등록 : http://localhost:8080/front-controller/v2/members/new-form
    • 목록 : http://localhost:8080/front-controller/v2/members

 

 


 

4. Model 추가 - v3

 

✔ 서블릿 종속성 제거

컨트롤러 입장에서 HttpServletRequest, HttpServletResponse 가 꼭 필요하지 X요청파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 컨트롤러가 서블릿 기술을 몰라도 동작 가능request를 Model로 사용하지 않고, 별도의 Model 객체를 만들어서 반환하기

 

 

✔ 뷰 이름 중복 제거

/WEB-INF/views/ ~~ .jsp 를 각 컨트롤러마다 반복해서 작성 -> 중복컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 VIew Resolver를 이용하여 처리하도록 단순화-> 나중에 다른 경로로 변경되더라도 프론트 컨트롤러만 수정하면 됨

 

 

v3 구조

 

 

 

1) ModelView (Model, View 담는 객체)

  • 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, View 이름도 전달하는 객체 
  • viewName : 뷰의 논리적 이름 저장
  • model : 모델 객체 저장 (뷰에 필요한 데이터를 key, value로 넣어줌) 

 

package hello.servlet.web.frontcontroller;

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

public class ModelView {

    private String viewName;                                // 뷰의 논리적 이름
    private Map<String, Object> model = new HashMap<>();    // 모델

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

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public Map<String, Object> getModel() {
        return model;
    }

    public void setModel(Map<String, Object> model) {
        this.model = model;
    }
}

 

 

2) ControllerV3 (인터페이스) 

  • 서블릿을 사용하지 X 
  • 프론트 컨트롤러에서 request에 담긴 요청 파라미터를 모두 꺼내서 paramMap에 담은 후 컨트롤러 호출
  • -> 이 인터페이스를 구현한 각 컨트롤러들은 응답결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체 반환

 

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {

    // 이전 컨트롤러와 달리 서블릿에 종속적이지 않음
    ModelView process(Map<String, String> paramMap);
}

 

 

3) 컨트롤러 (인터페이스 구현)

 

(1) MemberFormControllerV3 - 회원등록 컨트롤러

  • ModelView를 생성할 때 new-form 이라는 view의 논리적 이름 지정
    (이 논리이름을 가진 ModelView 객체 반환)

 

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

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberFormControllerV3 implements ControllerV3 {

    @Override
    public ModelView process(Map<String, String> paramMap) {
        // 뷰의 논리적 이름만 저장
        return new ModelView("new-form");
    }
}

 

 

(2) MemberSaveControllerV3 - 회원저장 컨트롤러 

  • 프론트 컨트롤러에서 이 컨트롤러를 호출하기 전에 request에 담긴 모든 요청파라미터를 조회해서 paramMap에 담은 뒤 이 컨트롤러 호출 (-> paramMap에는 request에 담긴 모든 요청파라미터들이 담겨있음)
  • paramMap.get("username") : request에 담겨있던 요청파라미터 조회 가능
  • new ModelView("save-result") : ModelView 객체를 생성하면서 "save-result"라는 view의 논리이름 지정
  • mv.getModel().put("member", member) : ModelView 객체의 model에 member 객체 저장 
    ( => ModelView 객체 : Model, View 객체 저장 ! ) 

 

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

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

import java.util.Map;

public class MemberSaveControllerV3 implements ControllerV3 {

    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        // 요청파라미터 값을 서블릿의 request에서가 아닌 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;
    }
}

 

 

(3) MemberListControllerV3 - 회원목록 컨트롤러

 

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

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

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

public class MemberListControllerV3 implements ControllerV3 {

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

 

 

4) FrontControllerServletV3 - 프론트 컨트롤러 

  • 프론트 컨트롤러에서 처리하는 기능 ↑
  • createParamMap() : request에 담긴 모든 요청파라미터를 조회해서 paramMap에 모두 넣음
  • controller.process(paramMap) : 넘겨받은 paramMap (모든 요청파라미터 정보가 들어있는)으로 각 컨트롤러에서 작업 수행한 뒤 Model과 View 반환
  • viewResolver
    • MyVIew view = viewResolver(viewName)
      • 컨트롤러가 반환한 논리 뷰 이름 -> 실제 물리 뷰 경로로 변경
      • 만들어진 물리 뷰 -> MyView 객체에서의 viewPath
    • view.render(mv.getModel(), request, response)
      • request의 요청파라미터를 model에 담았으니까 model도 같이 넘겨줌 (MyView 객체에 메소드 새로 생성)
      • model에 있는 요청파라미터를 다 뽑아서 다시 request 객체에 넣음
      • JSP는 서블릿의 request 객체에서 값을 꺼내기 때문 (request.getAttribute())
      • JSP로 포워드해서 JSP를 렌더링 (뷰 객체를 통해 HTML 화면 렌더링) 

 

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import 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/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;
        }

        // request에 담긴 요청 파라미터를 모두 꺼내서 paramMap에 담음 (메소드를 따로 뽑아줌 ctrl+alt+m)
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        String viewName = mv.getViewName();       // 뷰의 논리이름 추출 (new-form)
        MyView view = viewResolver(viewName);     // 뷰의 논리이름을 가지고 실제 뷰의 경로 생성(물리이름) (얘도 메소드로 뽑아줌)

        view.render(mv.getModel(), request, response);  // 모델도 같이 넘겨줘야 함
    }

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

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

 

 

5) MyVIew - 뷰 객체 

 

package hello.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;
import java.util.Map;

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 {
        // model에 있는 key, value를 다 뽑아서 request 객체에다가 다 넣음 (메소드 추출)
        // jsp는 서블릿의 request 객체에 넣어야 값을 편하게 꺼낼 수 있기 때문에
        modelToRequestAttribute(model, request);
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

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

 

 


 

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

 

✔ ModelView 객체 제거

항상 ModelView 객체를 생성하고 반환해야 하는 번거로움

 

-> 컨트롤러가 ModelView 객체를 반환하지 않고, viewName만 바로 반환

 

 

 

 

1) ControllerV4 (인터페이스)

  • ModelView를 반환하지 않음!
  • model 객체를 바로 파라미터로 전달, 바로 사용 -> 뷰의 이름만 반환

 

package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {

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

 

 

 

 

2) 컨트롤러

 

(1) MemberFormControllerV4 - 회원등록 컨트롤러

  • model을 파라미터로 받아서 바로 사용 -> 뷰이름 바로 반환

 

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

import hello.servlet.web.frontcontroller.v4.ControllerV4;

import java.util.Map;

public class MemberFormControllerV4 implements ControllerV4 {

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        // 뷰이름 바로 반환
        return "new-form";
    }
}

 

 

(2) MemberSaveControllerV4 - 회원저장 컨트롤러 

  • 파라미터로 받은 model에 바로 저장

 

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

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

import java.util.Map;

public class MemberSaveControllerV4 implements ControllerV4 {

    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에 바로 넣어주기
        model.put("member", member);
        return "save-result";
    }
}

 

 

(3) MemberListControllerV4 - 회원목록 컨트롤러

 

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

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

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

public class MemberListControllerV4 implements ControllerV4 {

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

 

 

3) FrontControllerServletV4 (프론트 컨트롤러) 

  • Model 객체를 여기서 생성 -> controller에 전달 -> 뷰 이름 바로 반환받아서 사용

 

package hello.servlet.web.frontcontroller.v4;

import hello.servlet.web.frontcontroller.ModelView;
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);
        // Model 만들어줌
        Map<String, Object> model = new HashMap<>();
        // Model도 같이 넘겨줌 -> view이름 반환
        String viewName = controller.process(paramMap, model);

        MyView view = viewResolver(viewName);       
        view.render(model, request, response);      // 모델도 같이 넘겨줘야 함
    }

    private static MyView viewResolver(String viewName) {

        return new MyView("/WEB-INF/views/" + viewName + ".jsp");

    }

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

 

 

 


 

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

 

어떤 컨트롤러도 매핑 가능

어떨 때는 Controllerv3, 어떨 때는 Controller4를 사용하고 싶을 때 -> 핸들러 어댑터

-> 컨트롤러 인터페이스 호환 가능

 

 

v5 구조

 

 

  • 핸들러 어댑터 : 프론트 컨트롤러와 컨트롤러 사이에서 다양한 컨트롤러를 호출할 수 있도록 어댑터 역할
  • 핸들러 : 다양한 컨트롤러를 받을 수 있도록 확장한 개념 (핸들러 > 컨트롤러)
  • 요청 URI 를 통해 핸들러 매핑 정보 조회 -> 핸들러 어댑터 목록 중에서 핸들러를 처리할 수 있는 핸들러 어댑터 조회 -> 핸들러 어댑터를 통해 핸들러(컨트롤러) 조회 -> ModelView 반환 -> 뷰 렌더링 

 

 

1) MyHandlerAdapter (어댑터용 인터페이스)

  • boolean supports(Object handler)
    • handler = 컨트롤러
    • 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메소드
  • ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
    • 프론트 컨트롤러가 아닌 이 어댑터가 실제 컨트롤러를 호출하고, 그 결과 ModelView 반환
    • 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환

 

package hello.servlet.web.frontcontroller.v5;

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

    // 실제 컨트롤러를 호출해서 ModelAndView 객체 반환
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;

}

 

 

2) ControllerV3HandlerAdapter (어댑터)

  • supports()
    • 매개변수로 받은 handler(컨트롤러)가 ControllerV3으로 형변환될 수 있는지 판단
    • true -> 이 어댑터 클래스는 ControllerV3을 처리할 수 있는 어댑터
  • handle()
    • handler(컨트롤러)를 ControllerV3으로 형변환
      (supports() 를 통해 ControllerV3만 지원하도록 걸렀기 때문에 형변환 O)
    • request 객체에 있는 요청파라미터를 paramMap에 담아서 ControllerV3 호출 -> ModelView 반환

 

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

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import 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) {
        // handler(컨트롤러)가 ControllerV3로 형변환될 수 있는지 판단
        return (handler instanceof ControllerV3);
    }

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

        // 위 supports 메소드에서 handler가 ControllerV3로 캐스팅될 수 있는 경우로 한번 걸렀기 때문에 캐스팅 가능
        ControllerV3 controller = (ControllerV3) handler;

        // request에 담긴 요청파라미터들을 paramMap에 담음
        Map<String, String> paramMap = createParamMap(request);
        // ModelView 반환
        ModelView mv = controller.process(paramMap);

        return mv;
    }

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

}

 

 

3) FrontControllerServletV5 (프론트 컨트롤러)

  • Map<String, Object> handlerMappingMap : 모든 컨트롤러를 받기 위함 (어떤 것이라도 URL에 매핑해서 사용가능)
  • List<MyHandlerAdapter> handlerAdapters : 핸들러 어댑터 목록을 담기 위함

  • 핸들러 매핑
    • Object handler = getHandler(request)
      • handlerMappingMap에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
  • 핸들러(컨트롤러)를 처리할 수 있는 어댑터 조회
    • MyHandlerAdapter adapter = getHandlerAdapter(handler) 
      • adapter.supports(handler)를 통해 handler(컨트롤러)를 처리할 수 있는 어댑터 조회
      • 어댑터가 handler(컨트롤러)를 처리할 수 있다면 ( = handler가 ControllerV3 인터페이스를 구현했다면), ControllerV3HandlerAdapter 객체(어댑터) 반환
  • 어댑터 호출 (어댑터에서 처리)
    • ModelView mv = adapter.handle(request, response, handler)
      • handle(request, response, handler)를 통해 실제 어댑터 호출
      • 어댑터 호출(실행) 결과를 어댑터에 맞춰서 반환 (이 MyHandlerAdapter 인터페이스는 ModelView 반환)

 

package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
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 {

    // 아무 컨트롤러나 다 받기 위해서 Object
    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    // 핸들러 어댑터 목록을 담기 위한 변수
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();


    public FrontControllerServletV5() {
        initHandlerMappingMap();    // 모든 컨트롤러를 다 받을 수 있는 핸들러매핑(handlerMappingMap) 초기화 -> 메소드로 추출
        initHandlerAdapters();      // 핸들러 어댑터 목록 초기화 (ControllerV3 핸들러어댑터를 담아둠)
    }


    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 request, HttpServletResponse response) throws ServletException, IOException {

        // 요청 url에 맞는 핸들러(컨트롤러) 반환 (메소드 추출)
        Object handler = getHandler(request);

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

        // 핸들러 어댑터 목록 조회해서 핸들러를 처리할 수 있는 어댑터 반환
        MyHandlerAdapter adapter = getHandlerAdapter(handler);
        // 핸들러 어댑터의 handler() 호출 -> ModelView 객체 반환
        ModelView mv = adapter.handle(request, response, handler);

        // ViewResolver 호출 -> 뷰 렌더링
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

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


    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
    //MemberFormControllerV3
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;                 // adapter가 handler를 처리할 수 있으면 adapter 반환
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler= " + handler);
    }

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

}

 

 

 

7. 유연한 컨트롤러2 - v5 (ControllerV4 추가)

 

 

1) FrontControllerServletV5 (프론트 컨트롤러)

  • 핸들러 매핑(handlerMappingMap)에 ControllerV4를 사용하는 컨트롤러 추가
  • ControllerV4 컨트롤러를 처리할 수 있는 ControllerV4HandlerAdapter 어댑터 추가
  • 위 코드들을 따로 빼서 의존성 주입하면 -> 프론트 컨트롤러를 건들지 않아도 다양한 컨트롤러 처리를 다 할 수 있음

 

private void initHandlerMappingMap() {
    handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
    handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
    handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

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

private void initHandlerAdapters() {
    handlerAdapters.add(new ControllerV3HandlerAdapter());
    // V4 어댑터 추가
    handlerAdapters.add(new ControllerV4HandlerAdapter());
}

 

 

2) ControllerV4HandlerAdapter (MyHandlerAdapter 인터페이스를 구현한 ControllerV4 어댑터)

  • supports()
    • handler(컨트롤러)가 ControllerV4인 경우에만 처리하는 어댑터
  • handle()
    • handler를 ControllerV4로 형변환 (ControllerV4는 paramMap, model을 받아서 viewName 반환)
    • 컨트롤러를 호출(실행)해서 viewName 반환
  • 어댑터 변환 (어댑터의 역할 !!)
    • ModelView mv = new ModelView(viewName);
      mv.setModel(model);
    • 어댑터는 MyHandlerAdapter를 구현했기 때문에 ModelView를 반환해야 함 !
      -> 해당 컨트롤러의 반환값(String viewName) 을 어댑터의 반환값(ModelView)으로 변환 !

 

*ControllerV4와 어댑터

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

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

 

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

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) {
        // handler(컨트롤러)가 ControllerV4인 경우에만 처리
        return (handler instanceof ControllerV4);
    }

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

        // handler(컨트롤러)를 ControllerV4로 형변환
        ControllerV4 controller = (ControllerV4) handler;

        // ControllerV4는 paramMap, model을 받아서 viewName 반환

        // paramMap 생성 (request의 요청파라미터들을 paramMap에 담음)
        Map<String, String> paramMap = createParamMap(request);
        // model 생성
        Map<String, Object> model = new HashMap<>();

        // 컨트롤러 호출(실행) 후 viewName 반환
        String viewName = controller.process(paramMap, model);

        // 해당 컨트롤러의 반환값(viewName)을 이 어댑터의 반환값(ModelView)으로 변환! -> 핸들러 어댑터의 역할!
        ModelView mv = new ModelView(viewName);
        // ModelView 객체에 viewName, model 넣어줌
        mv.setModel(model);

        return mv;
    }

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

 

 

 

=> 스프링 MVC의 핵심 구조