자바 백엔드 웹 기술의 진화과정(2) - 웹 프론트 컨트롤러 v1, v2, v3
서블릿 → JSP → MVC패턴 → 프론트 컨트롤러 패턴 → 스프링MVC
프론트 컨트롤러 특징
- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
- 요청에 맞는 컨트롤러를 찾아서 호출
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
- 단계적으로 도입
- v1 : 프론트 컨트롤러 패턴의 도입
- v2 : view render를 처리해주는 MyView 도입
- v3 : 서블릿 종속성 제거 및 뷰의 물리경로를 가지는 뷰 리졸버의 사용
- v4 : v3코드에서 컨트롤러가 직접 뷰의 논리 이름을 반환
- v5 : 어댑터(Adapter) 패턴의 도입 → 다양한 종류의 컨트롤러 처리
- FrontController는 스프링 웹 MVC의 DispatcherServlet 으로 진화한다.
V1 - 프론트 컨트롤러 패턴 도입
ControllerV1 인터페이스
public interface ControllerV1 {
//로직수행 추상 메서드
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
- 비즈니스 로직을 수행 할 수 있는 process 추상 메서드를 가진다.
MemberFormControllerV1 - 회원 등록 컨트롤러
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//view로 데이터 전달
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
- 회원가입 viewPath를 생성후에, 해당 viewPath로 dispatch
MemberSaveControllerV1 - 회원 저장 컨트롤러
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);
//view로 데이터 전달
String viewPath="/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
- request객체에 담긴 Parameter로 member객체를 memberRepository에 저장
- viewPath를 생성후 dispatch
MemberListControllerV1 - 회원 목록 컨트롤러
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);
//view로 데이터 전달
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
memberRepository
의 member객체들을setAttribute()
메서드를 이용해서 request에 저장- viewPath 생성 후 dispatch
FrontControllerServletV1 - 프론트 컨트롤러
@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");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
@WebServlet
의 urlPatterns :/front-controller/v1
으로 시작하는 모든 url path를 처리ControllerMap
: url에 해당되는 컨트롤러를 매핑service()
: requestURI 를 조회 → 호출할 컨트롤러를controllerMap
에서 찾고 없으면 404(SC_NOT_FOUND) 상태 코드를 반환
정리
- 프론트 컨트롤러에 url을 파싱
- url에 해당하는 controller객체 생성
controller.process()
수행
V2 : view render를 처리해주는 MyView 도입
- 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있어서 별도로 뷰를 처리하는 객체 생성
- 중복되는 코드
String viewPath = "/WEB-INF/views/???.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
ControllerV2 인터페이스
public interface ControllerV2 {
//V1과 유사하나 process의 리턴 타입이 MyView로 변경
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
- 모든 컨트롤러가 MyView를 리턴하도록 변경됨
MyView
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);
}
}
HttpServletRequest
/HttpServletResponse
객체를 받아서 dispatch를 수행하는 객체
MemberFormControllerV2 - 회원 등록 폼
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");
}
}
MemberSaveControllerV2 - 회원 저장
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");
}
}
MemberListControllerV2 - 회원 목록
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");
}
}
프론트 컨트롤러 V2
@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 view = controller.process(request, response);
view.render(request, response);
}
}
정리
- 매핑된 Controller를 호출하고 process를 수행하면 MyView를 반환 받는다
- 반환받은 MyView의
render()
메서드를 호출해서 forward로직을 수행하게 한다.
V3 - Model 추가
- 컨트롤러에서
HttpServletRequest / HttpServletResponse
를 사용하지 않아도 매개변수로 받음
→ 불필요한 객체 / Servlet에 종속적 HttpServletRequest
를 Model로 사용 → Model을 별도로 생성하도록 변경
V3에서 개선할 사항
- Servlet종속성 제거 → Model추가
- View path 중복 제거 → View Resolver 추가
ControllerV3 인터페이스
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
process()
메서드는Map
을 매개변수로 받으며, `ModelView`를 반환해야 하도록 변경
ModelView
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;
}
}
- Servlet의 종속성을 제거하기 위해 별도추가한 Model객체
- view에 가져갈 데이터가 있는 HashMap과 실제 물리주소인 viewName을 가진다.
MemberFormControllerV3 - 회원 등록 폼
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
- ModelView를 생성할 때 new-form 이라는 논리 이름을 지정.
MemberSaveControllerV3 - 회원 저장
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;
}
}
- 저장 로직 수행후 마찬가지로 ModelView에 “save-result” 라는 논리이름을 지정
- 또한 ModelView의 model멤버변수에 저장한 멤버객체를 담아서 반환
MemberListControllerV3 - 회원 목록
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;
}
}
- members라는 논리 이름을 지정한 ModelView객체 생성
- 해당 ModelView객체의 model멤버 변수에 members 데이터를 추가한뒤 반환
프론트 컨트롤러V3
@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에 request정보들을 담은 createParamMap()메서드를 수행한후 컨트롤러에 넘김
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
//논리 주소를 받은뒤 실제 물리주소로 만들어서 반환해주는 메서드
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
//request에 있는 Parameter들을 paramMap에 담아서 반환해줌
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;
}
}
Map<String, String> createParamMap()
: request에 있는 Parameter들을 사용할 수 있게 paramMap에 담아주는 메서드MyView viewResolver()
: 넘어온 viewName(논리주소)를 받아서 실제 물리주소로 만들어서 반환 해주는 메서드service()
- requestURI를 컨트롤러에 넘겨서 어떤 컨트롤러를 사용할지 선택
- 선택된 컨트롤러의
process()
메서드에paramMap
을 넘겨서 해당 컨트롤러에서 파라미터를 사용해서 로직수행후 ModelView를 받음 - ModelView에 있는 논리주소를
viewResolver()
메서드에 넘겨서 실제 물리주소로 변환 - MyView의
render()
메서드에 실제 주소를 넘겨서 forward시킴
정리
- process() 메서드의 코드 양 변화
반응형