이전 포스팅에서 핵심은 공통 처리에 대한 어려움
이었다.
ViewPath부터 (request, response) 객체 forward 코드까지 중복되는 게 많았다.
이러한 문제를 해결하기 위해서는 공통적으로 처리될 수 있는 부분은 컨트롤러가 호출되기 전에 처리되어야 한다.
프론트 컨트롤러(Front Controller) 패턴
을 사용해서 해결할 수 있는데, 스프링 MVC
의 핵심도 프론트 컨트롤러에 있다.
서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다.
각 컨트롤러들은 이 인터페이스를 구현하면 된다.
프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
이전 챕터에서 구현한 로직을 최대한 유지해 보자.
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
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)
// Model에 데이터 보관
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
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);
}
}
내부 로직은 기존 서블릿과 거의 같다.
@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 {
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
urlPatterns = "/front-controller/v1/*"
*
는 상위 경로 /front-controller/v1/
를 포함한 하위 경로 모두 이 서블릿에서 받아들인다는 의미다.
/front-controller/v1
/front-controller/v1/depth1
/front-controller/v1/depth1/depth2
private Map<String, ControllerV1> controllerMap = new HashMap<>()
requestURI
를 조회해서 실제 호출할 컨트롤러를 controllerMap
에서 찾는다.404(SC_NOT_FOUND)
상태 코드를 반환한다.controller.process(request, response);
을 호출해서 해당 컨트롤러를 실행한다.MVC
것을 그대로 사용한다. http://localhost:8080/front-controller/v1/members/new-form
http://localhost:8080/front-controller/v1/members
프론트 컨트롤러 패턴
을 도입해서 공통 처리를 진행했지만,
아직도 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있다
String viewPath = "경로";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
컨트롤러에서 직접 JSP
를 forward
해주는 과정이 사라지고,
컨트롤러에서 MyView
인터페이스의 render()
메서드를 호출해 JSP
로 forward
하도록 구조가 변경되었다.
jsp 경로
를 viewPath
로 받아 생성되며, render()
메서드 호출 시 request
, response
를 인자로 forward
한다. 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);
}
}
MyView
라는 점이 다르다. public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
public class MemberSaveControllerV2 implements ControllerV2 {
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);
Member savedMember = memberRepository.save(member);
// Model에 데이터 보관
request.setAttribute("member", member);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
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");
}
}
이제 모든 컨트롤러에서 공통적으로 구현돼있던 dispatcher.forward()
가 사라지고 반환 타입인 MyView
객체를 생성하여 반환해 주고 있다.
@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 {
String requestURI = request.getRequestURI();
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView myView = controller.process(request, response);
myView.render(request, response);
}
}
MyView myView = controller.process(request, response);
MyView
객체를 반환 타입으로 반환받는다. MyView
객체의 render(request, response)
메서드를 호출하면 forward
로직이 수행되어 JSP가 실행된다. MyView
객체를 사용함으로 각각의 컨트롤러가 forward
로직을 제대로 수행하고 있는지 신경 쓸 필요가 없다. 이번에는 서블릿 종속성
및 뷰 이름 중복
을 제거해 보자.
컨트롤러 입장에서 HttpServletRequest
, HttpServletResponse
이 필요하지 않는 상황이 있다.
컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경하면 구현 코드도 매우 단순해지고, 테스트 코드 작성이 쉽다.
컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화하자.
나중에 폴더 위치가 변경돼도 프론트 컨트롤러만 고치면 된다.
/WEB-INF/views/new-form.jsp
👉 new-form
/WEB-INF/views/save-result.jsp
👉 save-result
/WEB-INF/views/members.jsp
👉 members
서블릿의 종속성을 제거하기 위해 Model
을 직접 만들고, View
이름까지 전달하는 객체를
만들어보자.
이번에는 컨트롤러에서 HttpServletRequest
를 사용할 수 없기 때문에 Model
이 별도로 필요하다.
Model
과 View
이름까지 저장하는 객체@Getter @Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
Request
, Response
객체를 인자 값으로 받지 않기에 서블릿 기술로부터 종속성이 사라졌다.
HttpServletRequest
에서 필요한 정보는 프론트 컨트롤러에서 paramMap
에 담아 호출하면 된다.
MemberFormControllerV3 - 회원 등록 폼
ModelView
생성자로 들어가는 이름은 view
의 논리명으로 프론트컨트롤러에서 viewResolver
에 의해 물리명으로 조합된다.
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
전달받은 paramMap
에서 필요한 요청 파라미터를 꺼내어 사용한다.
Model
의 담고 싶은 데이터는 ModelView
객체의 Model
필드에 담도록 한다.
public class MemberSaveControllerV3 implements ControllerV3 {
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);
Member save = memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
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;
}
}
@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;
}
// paramMap
HashMap<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView myView = viewResolver(viewName);
myView.render(mv.getModel(), request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private HashMap<String, String> createParamMap(HttpServletRequest request) {
HashMap<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
HashMap<String, String> paramMap = createParamMap(request);
request
에서 파라미터 정보를 담아 Map
으로 담아준다. MyView myView = viewResolver(viewName);
MyView
객체를 반환해 준다. myView.render(mv.getModel(), request, response);
render()
메서드와는 파라미터가 다르다. render()
메서드를 만들어줘야 한다.render(mv.getModel(), request, response)
에 대응하는 메서드를 오버 로딩하자.
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 {
modelToRequestAttribute(model, request);
render(request, response);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach(request::setAttribute);
}
}
http://localhost:8080/front-controller/v3/members/new-form
http://localhost:8080/front-controller/v3/members