개발자 미니민의 개발스터디

[스프링 MVC 1편] MVC 프레임워크 만들기 (1)

by mini_min

 

이전 목차에서는 MVC 패턴을 적용해서 코드를 작성했다. 이번에는 프론트 컨트롤러 패턴을 학습하고 MVC 프레임워크를 만들어본다. 

 

 

프론트 컨트롤러 

여러 클라이언트가 서비스를 이용하여 컨트롤러를 호출한다고 했을 때, 각각의 컨트롤러가 갖는 공통된 부분들이 존재한다면, 이를 Front Controller 로 통합하여 한 번에 처리하고 이후 각각의 컨트롤러가 호출되게 한다.

 

 

프론트 컨트롤러의 특징

- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받는다.

- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출해준다.

- 입구가 하나! = 공통 처리가 가능해진다.

- 프론트 컨트롤러만 서블릿을 사용하고 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.

 

스프링 MVC 의 핵심도 바로 'Front Controller' 이다. 

스프링 MVC 의 DispatcherServlet 이 FrontController 패턴으로 구현되어 있다.

 


프론트 컨트롤러 도입 - V1

먼저 기존 코드에 프론트 컨트롤러를 도입해본다.

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

 

 

서블릿과 비슷한 모양의 컨트롤러 인터페이스를 만들었다. 이제 각 컨트롤러들은 이 인터페이스를 구현하면 된다!

 

package hello.servlet.web.frontcontroller.v1.controller;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.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);
}
}
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 javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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); //멤버 저장
//Model에 데이터를 보관한다.
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
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 javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
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);
}
}

 

내부 로직은 최대한 비슷하게 만들었다. 프론트 컨트롤러를 만들면 다음과 같다.

- 프론트 컨트롤러 V1

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 javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.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 {
//url 을 넣고 ControllerV1 을 호출해~ 이런식으로 짤거임.
private Map<String, ControllerV1> controllerV1Map = new HashMap<>();
//미리 저장해둠
public FrontControllerServletV1() {
controllerV1Map.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
controllerV1Map.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
controllerV1Map.put("/front-controller/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerV1Map.get(requestURI);
if ( controller == null)
{
//못찾았을 때 404 리턴하기
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//잘 찾았다면 넘어가기
controller.process(request, response);
}
}

 

프론트 컨트롤러 분석

urlPatterns  = /v1/* 으로 클라이언트에게 오는 하위 모든 요청은 이 서블릿에서 받아들인다.

controllerMap 을 만들어서 컨트롤러 URL 과 호출될 컨트롤러들을 미리 저장해준다. 이제 컨트롤러 URL 로 요청이 들어오면 해당 Controller 를 호출해서 사용하면 된다. 

 

service()

requestURI 를 조회해서 실제 호출할 컨트롤러를 map 에서 찾는다! 

컨트롤러를 찾았다면, 해당 컨트롤러의 process() 를 실행한다. 

 

JSP 는 이전과 동일한 것을 사용한다 :)

 


View 분리 - V2

이번에는 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있으므로, 이를 제거해본다. 뷰를 처리하는 별도의 객체를 만들어주면 된다.

 

 

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;
import java.util.Map;
import java.util.Objects;
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 로 적어서 공통된 부분을 처리했다. 아래 V2 버전의 컨트롤러들을 보면 훨씬 깔끔해진 것을 확인할 수 있다.

 

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

 

일단 ControllerV2 부터! 해당 컨트롤러로 반환되는 것이 MyView 가 되었다.

프론트 컨트롤러로 넘기는 것이 MyView 객체가 된 것이다. 

 

package hello.servlet.web.frontcontroller.v2.controller;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.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");
}
}
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 javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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); //멤버 저장
//Model에 데이터를 보관한다.
request.setAttribute("member", member);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
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 javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
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");
}
}

 

이제 각 컨트롤러들은 DisPatcher.forward() 를 직접 생성하지 않고 단순히 MyView 객체를 생성하고 거기에 뷰 이름을 넣고 반환하기만 하면 된다. 

 

프론트 컨트롤러 v2

package hello.servlet.web.frontcontroller.v2;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
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 javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.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 {
//url 을 넣고 ControllerV1 을 호출해~ 이런식으로 짤거임.
private Map<String, ControllerV2> controllerV1Map = new HashMap<>();
//미리 저장해둠
public FrontControllerServletV2() {
controllerV1Map.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
controllerV1Map.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
controllerV1Map.put("/front-controller/v2/members", new MemberListControllerV2());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV2 controller = controllerV1Map.get(requestURI);
if ( controller == null)
{
//못찾았을 때 404 리턴하기
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView view = controller.process(request, response);
view.render(request,response);
}
}

 

ControllerV2의 반환 타입이 MyView 이기 때문에 프론트 컨트롤러는 호출 결과로 MyView 를 반환 받는다. 그리고 프론트 컨트롤러는 반환받은 view 의 render() 를 호출하면 forward 로직을 수행해서 JSP 를 실행한다.

 

프론트 컨트롤러의 도입으로 MyView 객체의 render() 를 호출하는 부분을 모두 일관되게 처리할 수 있게 되었다!

 

 

 

 

 

 

 

 

 

블로그의 프로필 사진

블로그의 정보

개발자 미니민의 개발로그

mini_min

활동하기