MVC 패턴
만 적용했을 때는 공통 코드가 컨트롤러에 있어서, 클라이언트가 각각 컨트롤러를 호출해야 했다.
공통 코드가 프론트 컨트롤러에 있어서, 클라이언트의 요청은 모두 프론트 컨트롤러로 들어온다.
FrontController
패턴 특징프론트 컨트롤러 서블릿 하나로 모든 클라이언트의 요청을 받은 뒤, 프론트 컨트롤러가 각 요청에 맞는 컨트롤러를 찾아서 호출해준다.
이로 인해 여러 컨트롤러에 있던 공통 코드를 프론트 컨트롤러 하나로 모을 수 있고, 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용할 필요도 없어진다!
스프링 웹 MVC의 DispatcherServlet
도 FrontController
패턴으로 구현되어 있다.
v1
📌
V1
기존 코드를 최대한 유지하면서, 프론트 컨트롤러를 도입할 것이다!
ControllerV1
package hello.servlet.web.frontcontroller.v1;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
서블릿과 비슷한 컨트롤러 인터페이스를 도입함으로써
각 컨트롤러는 이 인터페이스를 호출하여 로직의 일관성을 가져갈 수 있게 되었다.
ControllerV1
인터페이스를 구현한 컨트롤러를 만들어볼 것이다.
지금 단계에서는 기존 로직을 최대한 유지하는 것이 핵심이다!
코드는 아래 링크 참고!
FrontControllerServletV1
- 프론트 컨트롤러이제 프론트 컨트롤러를 만들어서 위에 만든 컨트롤러를 호출할 수 있도록 만들어보자!
(전체 코드는 위에 있는 링크 참고!)
urlPatterns
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
/front-controller/v1
를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다./front-controller/v1
, /front-controller/v1/a
, /front-controller/v1/a/b
등controllerMap
private Map<String, ControllerV1> controllerMap = new HashMap<>();
key
: 매핑value
: 호출될 컨트롤러service()
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
requestURI
를 조회해서 실제 호출할 컨트롤러를 controllerMap
에서 찾는다.404(SC_NOT_FOUND)
상태 코드를 반환한다.controller.process(request, response);
를 호출해서 해당 컨트롤러를 실행한다.JSP
http://localhost:8080/front-controller/v1/members/new-form
에 접속해서 form 제출하면 아주 잘 된다!
기존 서블릿, JSP로 만든 MVC와 동일하게 실행되는 것을 확인할 수 있다!
View
분리 - v2
프론트 컨트롤러 패턴을 도입해서 모든 Request
요청에 대해서 프론트 컨트롤러를 거치게 함으로써 공통 처리를 모을 수 있었다.
하지만 여전히 모든 컨트롤러에서 뷰로 이동하는 부분에 중복되는 코드가 존재한다.😭
String viewPath = "경로";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
📌
V2
컨트롤러에서 뷰로 이동하는 중복 코드를 제거하기 위해, 별도로 뷰를 처리하는 객체를 만들자!
기존에 컨트롤러에서 바로 JSP를 forward
해주는 과정을 제거하고 MyView
를 프론트 컨트롤러에 반환한다.
그러면 프론트 컨트롤러는 MyView
인터페이스의 render()
를 호출하여 해당 인터페이스에서 JSP를 forward
하도록 구조를 바꿨다!
MyView
package hello.servlet.web.frontcontroller;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.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);
}
}
render()
메서드 호출 시 인자로 받은 request
, response
를 인자로 JSP로 forward
한다.
ControllerV2
package hello.servlet.web.frontcontroller.v2;
import hello.servlet.web.frontcontroller.MyView;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
ControllerV1
과 달리, 뷰를 반환한다!
dispatcher.forward()
를 직접 생성해서 호출하지 않아도 된다! MyView
객체를 생성하고 안에 뷰 이름만 넣고 반환해주면 된다.V1
과 비교하여 중복이 많이 제거되었다.😉FrontControllerServletV2
(전체 코드는 위의 링크 참고)
MyView view = controller.process(request, response);
ControllerV2
의 반환 타입이 MyView
이므로 프론트 컨트롤러의 호출 결과로 MyView
를 반환받는다.view.render()
를 호출하면 forward
로직을 수행해서 JSP가 실행된다.이번에도 잘 동작한다 ㅎㅎ
Model
추가 - v3
📌
V3
이번에는 서블릿의 종속성 및 뷰 이름 중복을 제거해보자!
지금까지 모든 컨트롤러에는 Request
, Response
객체를 인자값으로 전달해줬다. 하지만 모든 컨트롤러에서 이 인자들이 다 사용된 것은 아니었다.
🤔 이렇게 비효율적인 동작을 수정할 수는 없을까?
요청 파라미터 정보를 Map
으로 대신 넘기고, request
객체를 Model
로 사용하지 말고 별도의 Model
객체를 만들어서 반환하면 된다!
이제 컨트롤러가 서블릿을 전혀 사용하지 않도록 하여 구현 코드도 단순하게, 테스트 코드 작성도 쉽게 할 수 있도록 해보자!
그리고 컨트롤러에서 viewPath
를 지정할 때 다음과 같이 했다.
WEB-INF/views/new-form.jsp
WEB-INF/views/save-result.jsp
WEB-INF/views/members.jsp
계속 WEB-INF/views/
가 중복되었는데 이제 이렇게 전체 경로를 다 적어주지 말고 new-form
, save-result
등 단순화해보자!
이렇게 중복을 제거해주면 나중에 경로를 수정하게 될 때 한번에 수정할 수 있다.🤩
V3
구조V2
에서는 프론트 컨트롤러에서 바로 MyView
의 render()
를 호출해 JSP로 forward
해줬다. V3
에서는 그전에 viewResolver
를 호출해 MyView
를 반환하도록 했다. MyView
가 아니라 ModelView
객체를 반환해주도록 바뀌었다. ModelView
: 서블릿의 종속성을 제거하기 위해 별도의 Model
을 만들어 View
이름까지 전달하는 ModelView
객체를 만들었다.ModelView
package hello.servlet.web.frontcontroller;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Getter @Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
model
객체를 가지고 있다.model
은 단순히 map
으로 되어있으므로 컨트롤러에서 뷰에 필요한 데이터를 key
, value
로 넣어주면 된다.ControllerV3
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);
}
HttpServletRequest
가 제공하는 파라미터는 paramMap
에 담아서 호출해주면 된다.Model
데이터를 포함하는 ModelView
객체를 반환하면 된다.MemberFormControllerV3
- 회원 등록 폼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");
}
}
ModelView
를 생성할 때 뷰의 논리적인 이름을 new-form
으로 지정한다.viewResolver
로 처리한다.MemberSaveControllerV3
- 회원 저장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 {
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;
}
}
paramMap.get("username");
map
에 담겨져 있다. map
에서 필요한 요청 파라미터를 조회하면 된다.mv.getModel().put("member", member);
map
이므로 모델에 뷰에서 필요한 member
객체를 담고 반환한다.MemberListControllerV3
- 회원 목록 조회FrontControllerV3
(전체 코드는 위의 링크 참고)
MyView view = viewResolver(viewName)
: 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변환한다.
members
/WEB-INF/views/member.jsp
view.render(mv.getModel(), request, response)
: 뷰 객체를 통해 HTML 화면을 렌더링한다.
request.getAttribute()
로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내 request.setAttribute()
로 담아둔다. 이번에도 역시 잘 실행된다~
하지만 아직도 수정해야할 부분이 남았다. 다음 시간에 마저 알아보자!