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 화면 렌더링)
- MyVIew view = viewResolver(viewName)
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을 처리할 수 있는 어댑터
- 매개변수로 받은 handler(컨트롤러)가 ControllerV3으로 형변환될 수 있는지 판단
- handle()
- handler(컨트롤러)를 ControllerV3으로 형변환
(supports() 를 통해 ControllerV3만 지원하도록 걸렀기 때문에 형변환 O) - request 객체에 있는 요청파라미터를 paramMap에 담아서 ControllerV3 호출 -> ModelView 반환
- handler(컨트롤러)를 ControllerV3으로 형변환
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에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
- Object handler = getHandler(request)
- 핸들러(컨트롤러)를 처리할 수 있는 어댑터 조회
- MyHandlerAdapter adapter = getHandlerAdapter(handler)
- adapter.supports(handler)를 통해 handler(컨트롤러)를 처리할 수 있는 어댑터 조회
- 어댑터가 handler(컨트롤러)를 처리할 수 있다면 ( = handler가 ControllerV3 인터페이스를 구현했다면), ControllerV3HandlerAdapter 객체(어댑터) 반환
- MyHandlerAdapter adapter = getHandlerAdapter(handler)
- 어댑터 호출 (어댑터에서 처리)
- ModelView mv = adapter.handle(request, response, handler)
- handle(request, response, handler)를 통해 실제 어댑터 호출
- 어댑터 호출(실행) 결과를 어댑터에 맞춰서 반환 (이 MyHandlerAdapter 인터페이스는 ModelView 반환)
- ModelView mv = adapter.handle(request, response, handler)
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)으로 변환 !
- ModelView mv = new ModelView(viewName);
*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의 핵심 구조
'Spring' 카테고리의 다른 글
[인프런/스프링 MVC 1편] 6. 스프링 MVC - 기본 기능 (0) | 2023.05.09 |
---|---|
[인프런/스프링 MVC 1편] 5. 스프링 MVC - 구조 이해 (0) | 2023.05.02 |
[인프런/스프링 MVC 1편] 3. 서블릿, JSP, MVC 패턴 (0) | 2023.04.28 |
[인프런/스프링 MVC 1편] 2. 서블릿 (0) | 2023.04.26 |
[인프런/스프링 MVC 1편] 1. 웹 애플리케이션 이해 (1) | 2023.04.20 |