목차
1. 프론트 컨트롤러
1) 개요
프론트 컨트롤러 도입 전
프론트 컨트롤러 도입 후
프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받고, 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출하는 구조이다. 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다. 스프링의 DispatcherServlet이 바로 FrontController 패턴으로 구현되어 있다.
2) 도입
구조
2) 코드 예제
먼저 인터페이스로 전송과 저장을하는 컨트롤러를 구현한다.
ControllerV1.interface
public interface Controller1 {
void logic(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
FormControllerV1.class(컨트롤러 구현체, 전송역할)
public class FormController1 implements Controller1 {
@Override
public void logic(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FormController1.logic");
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
SaveControllerV1.class(컨트롤러 구현체, 저장역할)
public class SaveController1 implements Controller1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void logic(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("SaveController1.logic");
String name = request.getParameter("name");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(name, age);
memberRepository.save(member);
String viewPath = "/WEB-INF/views/member-save.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
이제FrontController를 만들어보자.
FrontController1.class
@WebServlet(name = "frontController1", urlPatterns = "/mvc-pattern/controller1/*")
public class FrontController1 extends HttpServlet {
Map<String, Controller1> controllerMap = new HashMap<>();
public FrontController1() {
controllerMap.put("/mvc-pattern/controller1/form", new FormController1());
controllerMap.put("/mvc-pattern/controller1/save", new SaveController1());
controllerMap.put("/mvc-pattern/controller1/list", new ListController1());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
Controller1 controller1 = controllerMap.get(requestURI);
controller1.logic(req, resp);
}
}
주소창에 "/mvc-pattern/controller1/form"를 입력하면 어떻게 되는지 알아보자.
1. 먼저 urlPatterns = "/mvc-pattern/controller1/*" 이렇게 작성하면 controller1/ 뒤에 어떤 값이 오더라도, FrontController가 가장 먼저 호출 되게 된다.이후 호출되는 과정에서 생성자를 통해 controllerMap에 키에는 Url, 값에는 직전에 구현해놓은 컨트롤러의 객체를 담게 된다.
2. 그 다음 logic() 메서드가 실행되는데 request.getRequestURI()를 사용하면 주소창에 입력한 url이 들어온다. "/mvc-pattern/controller1/form". 이를 키값으로 FormController1의 객체를 꺼내오게 된다.
3.controller1의 logic 메서드가 실행된다.
4. "/WEB-INF/views/new-form.jsp" 로 서버 내부 forward를 진행하게 되고, new-form.jsp에 있던 html 코드를 클라이언트에 응답해주게 된다.
여기서 중요한 것은 역할과 구현을 나눌 수 있는 인터페이스의 활용이다. 하지만 구조만 바뀌었을 뿐, 저번 포스팅에서의 코드와 다른점이 별로 없다. 여전히 view로 넘기는 코드는 중복이 많고 그대로이다.
//View로 넘기기
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
2. View 분리
1) 개요
view 분리 후
기존에는 Controller에서 JSP로 forward하였지만 이번엔 MyView를 사용한다. 결론부터 말하자면 MyView에 jsp 렌더링을
위임한 코드이다. 간단한 코드를 통해 알아보자.
2) 코드 예제
ViewRenderer.class
public class ViewRenderer {
String viewPath;
public ViewRenderer(String viewPath) {
this.viewPath = viewPath;
}
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
viewPath를 Controller에서 생성자로 입력받고, view를 jsp로 이동시켜주는 역할을 한다.
FormController2.class
public class FormController2 implements Controller2 {
@Override
public ViewRenderer logic(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FormController2.logic");
String viewPath = "/WEB-INF/views/new-form.jsp";
ViewRenderer viewRenderer = new ViewRenderer(viewPath);
return viewRenderer;
}
}
SaveController2.class
public class SaveController2 implements Controller2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ViewRenderer logic(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("SaveController2.logic");
String name = request.getParameter("name");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(name, age);
memberRepository.save(member);
//Model에 데이터를 보관하기.
request.setAttribute("member", member);
//ViewRenderer 반환하기
String viewPath = "/WEB-INF/views/member-save.jsp";
ViewRenderer viewRenderer = new ViewRenderer(viewPath);
return viewRenderer;
}
}
FrontController2.class
@WebServlet(name = "frontController2", urlPatterns = "/mvc-pattern/controller2/*")
public class FrontController2 extends HttpServlet {
Map<String, Controller2> controllerMap = new HashMap<>();
public FrontController2() {
controllerMap.put("/mvc-pattern/controller2/form", new FormController2());
controllerMap.put("/mvc-pattern/controller2/save", new SaveController2());
controllerMap.put("/mvc-pattern/controller2/list", new ListController2());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("FrontController2.service");
String requestURI = req.getRequestURI();
Controller2 controller2 = controllerMap.get(requestURI);
//controller2의 url이 담긴 viewRenderer
ViewRenderer viewRenderer = controller2.logic(req, resp);
//view 이동
viewRenderer.process(req, resp);
}
}
자 이제 동작과정을 이해해보자.
1.url에 "/mvc-pattern/controller2/form"을 입력하면 FrontController2가 실행되게 된다. 이후 실행되면서 만들어둔 MemberControl 구현체들은 controlMap에 담기게 될 것이다.
2. Service 메서드가 실행되면 URI 키 값으로 controller를 꺼내게 된다. 이후 controller의 logic메서드를 통해 url이 담긴 viewRenderer를 return한다.
3. viewRenderer는 다시 process메서드의 인자로 request, response를 받아 dispatcher forward를 하게 된다.
이 코드에서 controller들은 viewRenderer에 이동할 방향만 넣고, 각각의 비지니스 로직에만 집중하게 된다. jsp를 사용한 view로 넘기는 부분은 viewRenderer가 담당하게 된다.
3. Model 추가
1) 개요
서블릿 입장에선 HttpServletRequest, HttpServletResponse가 당장 필요하지 않다. 요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다. 그리고 request 객체를 model로 사용하는 대신 별도의 Model 객체를 만들어 반환하면 된다.
뷰 이름도 너무 중복된다. 이는 컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치에서의 이름은 프론트 컨트롤러에서 처리하도록 단순화할 수 있다.
"WEB-INF/views/new-form.jsp" -> new-form
"WEB-INF/views/save-result.jsp" -> save-result
기존: Controller에서 ViewRenderer를 반환
변화: Controller에서 ModelView를 반환
1. 기존에는 request자체에 데이터를 담았지만, 현재는 ModelView가 데이터를 가지고 있다. 또한 간단한 "new-form" url 정보를 가지고 있게 된다.
2. Controller는 객체에 담아 FrontController로 던지기만 하고, FrontController는 이를 담아, ViewResolver에 보낸다. ViewResolver는 ModelView의 "new-form" 의 이름을 인자로 받고, "WEB-INF/views/new-form.jsp" 경로를 가진 myView를 return해 준다.
3. myView에서는 ModelView의 데이터를 받아 request.setAttribute에 넣는 변환과정을 거치게 된다.(forward할 때 jsp는 request에 있는 Attribute만 읽을 수 있기 때문이다.)
2) 코드 예제
ViewRenderer.class
public class ViewRenderer {
String viewPath;
public ViewRenderer(String viewPath) {
this.viewPath = viewPath;
}
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
public void process(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// model의 데이터를 jsp에서 사용할 수 있도록 request에 set해준다.
model.forEach((key, value) -> request.setAttribute(key, value));
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
ModelView.class
@Setter @Getter
public class ModelView {
String viewName;
Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
FormController3.class
public class FormController3 implements Controller3 {
@Override
public ModelView logic(Map<String, String> paramMap){
System.out.println("FormController3.logic");
String viewPath = "new-form";
ModelView modelView = new ModelView(viewPath);
return modelView;
}
}
SaveController3.class
public class SaveController3 implements Controller3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView logic(Map<String, String> paramMap){
System.out.println("SaveController3.logic");
//데이터를 저장소에 보관하기.
String name = paramMap.get("name");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(name, age);
memberRepository.save(member);
//View와 데이터 modelView에 넘기기
String viewPath = "member-save";
ModelView modelView = new ModelView(viewPath);
modelView.getModel().put("member", member);
return modelView;
}
}
코드설명
SaveController3는 기존의 SaveController2와 달리 request.setAtrribute 코드를 사용하지 않는다. 즉, request, response에 대한 의존성이 사라진 것이다. 그냥 ModelView에 논리 이름과 데이터를 저장하고 return하기만 한다.
FrontController3.class
@WebServlet(name = "frontController3", urlPatterns = "/mvc-pattern/controller3/*")
public class FrontController3 extends HttpServlet {
Map<String, Controller3> controllerMap = new HashMap<>();
public FrontController3() {
controllerMap.put("/mvc-pattern/controller3/form", new FormController3());
controllerMap.put("/mvc-pattern/controller3/save", new SaveController3());
controllerMap.put("/mvc-pattern/controller3/list", new ListController3());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// request의 정보를 저장할 paramMap
Map<String, String> paramMap = new HashMap<>();
// paramMap에 request 요청 정보 넣기
req.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
// 알맞은 controller 꺼내기
String requestURI = req.getRequestURI();
Controller3 controller3 = controllerMap.get(requestURI);
// controller 메서드 실행하기 -> viewPath정보와 paramMap를 가진 modelView
ModelView modelView = controller3.logic(paramMap);
//viewResolver: "new-form" -> "/WEB-INF/views/new-form.jsp"
String viewName = modelView.getViewName();
ViewRenderer viewRenderer = viewResolver(viewName);
// view 이동
viewRenderer.process(modelView.getModel(), req ,resp);
}
private ViewRenderer viewResolver(String viewName) {
return new ViewRenderer("/WEB-INF/views/" + viewName + ".jsp");
}
}
코드설명
1. 앞서 SaveController3에서 request를 사용할 수 없으므로, request.get으로 바로 데이터를 꺼내올 수 없다. 따라서 이를 우회하기 위해 FrontController에서 paramMap에 값을 넣어주고 이를 controller3에 넣어준다.
2. 이후 controller3에서 return 받은 ModelView를 ViewResolver에 넣어 물리이름을 반환해주게 된다.
3. 마지막으로 ModelView의 데이터,request, response를 인자로 ViewRenderer에 넣어주게 된다.
4. ViewRenderer에서는 ModelView의 데이터를 request에 넣어주고, dispatcher를 사용해 jsp로 서버 내 이동을 하게 된다.
4. 단순하고 실용적인 컨트롤러 구현
1) 개요
앞서 만든 Controller3은 서블릿 종속성을 제거하고 뷰 경로의 중복성을 제거하는 등 잘 설계된 컨트롤러이다. 하지만 항상 ModelView 객체를 생성하고 반환해야 하는 부분은 조금 번거롭다. 좋은 프레임워크는 아키텍쳐도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 아래의 구조로 개발하면 번거로운 부분을 해결할 수 있다. Controller는 ModelView에 따로 담지 않고 model 객체를 파라미터로 받고, 결과로 View의 이름만 반환해주면 된다.
2) 코드 예제
MyView.class
V3와 동일하다.
ModelView.class
없다.
FormController4.class
public class FormController4 implements Controller4 {
@Override
public String logic(Map<String, String> paramMap, Map<String, Object> model){
System.out.println("FormController4.logic");
String viewPath = "new-form";
return viewPath;
}
}
Form엔 비즈니스로직이 필요없으므로, viewName만 return해준다.
SaveController4.class
public class SaveController4 implements Controller4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String logic(Map<String, String> paramMap, Map<String, Object> model){
System.out.println("SaveController4.logic");
//데이터를 저장소에 보관하기.
String name = paramMap.get("name");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(name, age);
memberRepository.save(member);
//View와 데이터 modelView에 넘기기
String viewPath = "member-save";
model.put("member", member);
return viewPath;
}
}
코드설명
기존의 비지니스 로직 이후 밑에 인자로 받은 model에 값을 넣고 ViewName을 return한다.
FrontController4.class
@WebServlet(name = "frontController4", urlPatterns = "/mvc-pattern/controller4/*")
public class FrontController4 extends HttpServlet {
Map<String, Controller4> controllerMap = new HashMap<>();
public FrontController4() {
controllerMap.put("/mvc-pattern/controller4/form", new FormController4());
controllerMap.put("/mvc-pattern/controller4/save", new SaveController4());
controllerMap.put("/mvc-pattern/controller4/list", new ListController4());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// request의 정보를 저장할 paramMap
Map<String, String> paramMap = new HashMap<>();
Map<String, Object> model = new HashMap<>();
// paramMap에 request 요청 정보 넣기
req.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
// 알맞은 controller 꺼내기
String requestURI = req.getRequestURI();
Controller4 controller5 = controllerMap.get(requestURI);
String viewName = controller5.logic(paramMap, model);
ViewRenderer viewRenderer = viewResolver(viewName);
// view 이동
viewRenderer.process(model, req ,resp);
}
private ViewRenderer viewResolver(String viewName) {
return new ViewRenderer("/WEB-INF/views/" + viewName + ".jsp");
}
}
코드설명
ModelView를 사용하지 않고 FrontController에 model을 생성해 인자로 값을 넣어준다. 해시맵은 mutable이므로 인자로 들어가도 값을 변경할 수 있다는 것을 활용한 것이다. 이렇게 되면 Controller 안에서 값을 조작하고 따로 return해주지 않아도 된다. 그 밑의 코드는 ViewName을 viewResolver에 담아 url을 생성 -> ViewRenderer을 통해 jsp로 forward하는 기존과 동일한 코드이다.
본 포스팅은 김영한님 인프런 강의내용을 바탕으로 복습을 위해 작성하였습니다. 강의를 통해 배운 개념을 바탕으로 추가적으로 공부한 부분과 간단한 코드 예제를 작성하였습니다. 코드 전체를 복사한 것이 아니라 임의로 수정하거나 생략하였습니다.
'백엔드 > Spring' 카테고리의 다른 글
[Spring] 스프링 MVC - 구조 이해 (0) | 2022.07.11 |
---|---|
[Spring] MVC 패턴 핸들러, 어댑터 (0) | 2022.06.14 |
[Spring] 서블릿, MVC 패턴 (0) | 2022.06.10 |
[Spring] 서블릿 (0) | 2022.06.09 |
[Spring] 웹 애플리케이션 이해 (0) | 2022.06.02 |