SpringMVC (4) - MVC 프레임워크 만들기 [중요]
1. 프론트 컨트롤러 패턴 소개
- FrontController 패턴의 특징
> 하나의 서블릿으로 클라이언트 요청 받기
> 요청에 맞는 컨트롤러 찾아서 호출 (pojo)
> 입구를 하나로 만드는 것이 핵심 (공통처리,pojo를 활용한 확장성 < 서블릿 의존성 제거)
> 스프링 MVC의 핵심은 FrontController이다.
2. 프론트 컨트롤러 V1
v1의 구조
- V1의 핵심은 서블릿 1개로 만들고 컨트롤러 분리해서 처리하기이다.
- 기존 프론트 컨트롤러에 맵핑과 서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
-컨트롤러 v1
public interface ControllerV1{
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
- 각 컨트롤러는 위 인터페이스를 구현하여, 로직의 일관성 가져갈 수 있다.
- 프론터 컨트롤러로 들어온 request와 response를 넘겨주면, 서블릿에서 처럼 꺼내쓸 수 있음
> 회원 등록 컨트롤러
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 {
private 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);
}
}
> 회원 목록 컨트롤러
public class MemberListControllerV1 implements ControllerV1 {
private 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);
}
}
- 서블릿에서 수행하던 로직을 옮긴 것 뿐 -> 단 수행 객체는 서블릿이 아닌 pojo
> frontContrllerv1
@WebServlet(name="frontcontrollerServletV1", urlPatterns="/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet{
private Map<String,ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1(){
controllerMap.put("urlPath",new MemberFormControllerV1());
//url에 따라 ControllerV1들 맵핑
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String Uri = request.getRequestURI();
controllerV1 controller = controllerMap.get(Uri);
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request,response);
}
}
- 프론트 컨트롤러는 uri를 얻어서 맵핑 정보 중 해당 uri와 맵핑된 컨트롤러 찾아서 request,response 전달만 수행
- /*패턴으로 해당 url의 하위 요청 모두 받게 만듦
* 아직 뷰로 이동하는 부분 forward부분 중복됨 ! 중복 제거해보자
3. 프론트 컨트롤러 V2
- 모든 컨트롤러에서 뷰로 이동하는 부분 중복을 제거해보자
- 별도로 뷰를 처리하는 객체 만들기
- 핵심은 각 컨트롤러의 포워딩 부분을 MyView가 수행하도록하고, 각 컨트롤러는 viewPath를 담은 MyView를 리턴하게 함
v2의 구조
public class MyView{
private String viewPath;
public MyView(String viewPath){
this.viewPath = viewPath;
}
}
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
RequestDispatcher ds = request.getRequestDispathcer(viewPath);
ds.forward(request,response);
}
- 컨트롤러 v2
MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
-v2 회원 등록 컨트롤러
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 {
private 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");
}
}
- 회원 목록 컨트롤러
public class MemberListControllerV2 implements ControllerV2 {
private 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");
}
}
- 컨트롤러들이 직접 포워딩하지 않고, 포워딩 역할을 수행할 수 있는 MyView객체에 viewPath등록해서 return
- 프론트 컨트롤러 V2
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/frontcontroller/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 view = controller.process(request, response);
view.render(request, response);
}
}
- 기존 V1은 process만 호출했다면, V2는 process호출해서 MyView객체를 전달받고, 이를 통해 render를 호출하는 형식으로 바뀌었다.
- 이를 통해 각 컨트롤러는 중복되는 포워딩 부분을 Myview객체에게 맡기고, MyView만 반환하는 형식으로 교체됨
- 여기까지는 model사용안하고, request에 직접 저장
4. V3 Model 추가 (컨트롤러 서블릿 종속성 제거 - 매우 중요)
- 기존 컨트롤러는 process부분이 서블릿의 service와 매우 유사하여 서블릿 종속적인 컨트롤러였다.
- 이는 컨트롤러를 테스트하기 어렵게 만들고, request 밑 response를 사용하지 않아도 매개변수로 선언하게 하여 유연성을 낮춘다 이를 개선해보자.
- 또한 prefix와 suffix를 지정하여 view이름 중복도 제거해 보자
- model을 도입 컨트롤러는 request를 통해 파라미터 전달받지 않고, request에 처리결과를 저장하지 않기 때문에, request 필요없음
* 두가지 모델임 파람모델,저장모델
- 핵심은 프론트컨트롤러가 request로 전달받은 파라미터 모두 꺼내서 새로운 Map에 담아 컨트롤러에 전달 ->
컨트롤러는 전달받은 Map을 통해 로직 처리 후 Model객체를 생성하여 결과를 담고 return
-> 프론트 컨트롤러는 return받은 model을 view에 전달
> ModelView : request대신에 로직 처리 결과를 담고, viewPath를 담을 객체이다.
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() {this.viewName = viewName;}
public Map<String,Object> getModel() {return model;}
public void setModel(Map<String,Object> model) {this.model = model;}
* Map model은 request.setAttribute와 정확하게 같은 동작 수행
- 컨트롤러 V3
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
- 서블릿 종속성 제거, 처리 결과 ModelView반환
- 회원 등록 컨트롤러
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
- 회원 저장 컨트롤러
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;
}
- 모델에 뷰에 필요한 member객체 담아서 mv리턴
-회원 목록 컨트롤러
public class MemberListControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}
- 프론트 컨트롤러 v3
@WevServlet(name ="frontControllerServletV3",urlPatterns = "/front-fontroller/v3/*")
public class FrontControllerServletV3 extends HttpServlet{
private Map<String, ControllerV3> controllerMap = new HashMap<>();
public FrontControllerServletV3(){
//컨트롤러 v3와 url맵핑
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
String requestUri = request.getRequestURI();
ControllerV3 controller = controllerMap(requestURI);
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String,String> paramMap = new HashMap<>();
getParam(paramMap,request);
ModelView mv = controller.process(paramMap);
MyView view = viewResolver(mv.getViewName());
view.rander(mv.getModel(),request,response);
}
private Map<String,String> getParam(Map<String,String> paramMap,HttpServletRequest request) throws ServletException{
request.getParameterNames().asIterator().forEachRemaining(name -> paramMap.put(name,request.getParameter(name));
}
private MyView viewResolver(String viewName){
return new MyView("/WEB-INF/views/"+viewName+".jsp");
}
}
- paramMap에 request에 담긴 파라미터 전부 넘기기
- 뷰 리졸버를 통해 뷰이름 중복 제거, 이제 컨트롤러는 뷰의 논리 이름만 return하면됨
- view.render를 통해 HTML화면 랜더링함
- MyView에 추가 된 메서드
public void render(Map<String,Object> model, HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOExcpetion{
model.forEach((key,value) -> request.setAttribute(key,value));
//포워딩하는 것은 동일
}
- jsp는 request에 저장된 값을 꺼내기 때문에 따로 mv에 저장된 값을 다시 request로 옮기는 과정이 필요함
5. 단순하고 실용적인 컨트롤러 v4
- 앞서 만든 v3컨트롤러는 서블릿 종속성을 제거하고, 뷰 경로의 중복을 제거했음
- 한발 더 나아가서 컨트롤러가 ModelView를 리턴하는게 아니라 viewName만 리턴하도록 바꿔보자
- 핵심 로직은 request요청 정보를 담은 paramMap과 처리 결과를 담을 model을 컨트롤러에 매개변수로 주입하고, 컨트롤러는 메서드 실행결과로 viewName만 반환하도록 함
- 컨트롤러 인터페이스 v4
public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
- 회원 등록 컨트롤러
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object>
model) {
return "new-form";
}
}
- 회원 저장 컨트롤러
public class MemberSaveControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object>
model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
model.put("member", member);
return "save-result";
}
}
- 회원 목록 컨트롤러
public class MemberListControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object>
model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
- 프론트 컨트롤러 v4
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-
controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
private Map<String, ControllerV4> controllerMap = new HashMap<>();
public FrontControllerServletV4() {
controllerMap.put("/front-controller/v4/members/new-form", new
MemberFormControllerV4());
controllerMap.put("/front-controller/v4/members/save", new
MemberSaveControllerV4());
controllerMap.put("/front-controller/v4/members", new
MemberListControllerV4());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
private 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;
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
- 이전버전과 거의 동일 다만, 컨트롤러 내부에서 ModelView를 생성해서 return하는게 아니라,
- 프론트 컨트롤러로부터 주입받은 model에 처리결과를 담고, viewName을 리턴
- viewName과 ViewResolver를 통해 MyView객체를 만들고, model을 넘겨서 처리
참고자료: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1