1. 프론트 컨트롤러의 개념
이제 Controller의 공통적인 부분을 담당하는 Front Controller를 만들어본다. 여기서부터 Controller를 발전시켜나가다보면, 결국 스프링 MVC 프레임워크에 다다르게 된다.
기존에는 각 클라이언트가 필요한 정보에 따라 각 Controller에 요청하는 구조였지만, Front Controller로 Controller 간 공통적인 부분이 통합되어 모든 요청들이 Front Controller를 거치고 나서, 각 Controller 부분으로 전달되는 구조가 된다.
2. 프론트 컨트롤러 작성해보기(V1)
작성할 파일들의 구조는 다음과 같다. 각 페이지에 해당하는 컨트롤러인 MemberFormControllerV1, MemberListControllerV1, MemberSaveControllerV1 이 있다. 그리고 이 파일들을 만들기 전에, 컨트롤러들의 공통 항목들을 메서드로 뽑아낼 수 있는 ControllerV1 인터페이스를 만든다. 그리고 각 Controller가 ControllerV1을 상속받게 함으로써 다형성을 지켜서 Controller를 작성하게 된다.
ControllerV1
process라는 메서드를 만들고, request, response를 받아오도록 한다. exception 처리도 이전에 진행했던 MvcController와 같은 양식으로 만든다. 일단 클라이언트들의 요청을 받아서 request, response로 담고, 이 process 메서드로 오게 만들 것이다. 다형성을 이용해서 이 ControllerV1을 상속받는 각 Controller에서 process 메서드의 내용들을 작성한다. 즉 공통으로 request, response를 받아오고 exception을 처리하는 것은 동일하기 때문에 인터페이스에 메서드의 형태로 만들어 놓는다. 하지만 request, response를 받아와서 처리하는 로직 코드나, view 쪽(.jsp)으로 넘겨주는 주소 등은 각 Controller마다 다르다. 이것을 각 Controller에서 Override하여 다형성을 구현하게 된다.
1
2
3
4
|
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
|
cs |
처리 방향은 아래 도식과 같다.
Front Controller에 의한 처리 흐름도
각 Controller 파일
각 Controller에서 ControllerV1 인터페이스를 상속받도록 한다. ControllerV1의 process 메서드를 상속받도록 하고, 코드 내용은 이전에 진행한 MvcController와 동일한 방식으로 작성하여 로직을 실행하고 view쪽으로 이동될 수 있게 만든다. 앞서 말한 바와 같이 process 메서드를 override 해서 각 Controller에서 원하는 방식으로 로직을 진행할 수 있는 것이다.
-> MvcFormControllerV1 파일. 나머지 각 Controller들은 이전 글에서의 MvcController 부분과 동일한 코드이다.
1
2
3
4
5
6
7
8
|
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);
}
}
|
cs |
FrontControllerServletV1
이 부분이 프론트 컨트롤러의 핵심 부분이다. 여기서 Http 요청을 받아들이고, 분석하여 각 Controller로 보내는 역할을 하게 된다. 기존의 HttpServlet을 상속받는 것과 동일한 양식으로 작성한다.
urlPatterns 부분에서 /*으로 지정한 부분을 볼 수 있는데, 이렇게 지정하면 /front-controller/v1/ 을 포함한 그 하위 주소에 대한 모든 요청을 일단 이 Controller에서 처리하도록 해준다. 그래서 Http 요청 URI에 따라서 각 controller로 요청 정보를 전달해줄 수 있게 되는 것이다.
Map 형태로 String을 키값으로 받고, ControllerV1 타입을 value로 호출할 수 있도록 만든다. 그리고 request.getRequestURI() 메서드로 클라이언트가 요청한 url 주소를 담아서 Map으로 전달하고, 이에 따라 어떤 컨트롤러의 process 메서드를 실행할지를 결정하게 된다. 예를 들어, 클라이언트가 /front-controller/v1/members 라는 주소로 요청하면 생성자를 통해서 new MemberListControllerV1()에 의해 해당 인스턴스가 controller라는 변수로 담기고(17번째줄), 24번째 줄의 controller.process에 의해서 MemberListControllerV1에 정의해놓은 process 메서드가 실행되는 구조이다.
혹시 /front-controller/v1/hi 라는 URI로 요청이 들어오면, 생성자 부분에서 매핑이 되어있지 않아서 controller는 null 값을 가지게 되고, 이에 따라 response에 NOT_FOUND, 404를 담아서 그대로 return;을 하여 클라이언트에서 404 에러를 확인할 수 있게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@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 uri = request.getRequestURI();
ControllerV1 controller = controllerMap.get(uri);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
|
cs |
이제 도식에서 본 것과 같이 각 Controller에서 process 메서드를 이용하여 view 쪽으로 넘어가는 부분을 살펴본다.
3. View 분리(V2)
각 Controller에서 dispatcher를 불러오고, forward 하는 부분도 중복된 코드였다. 이 부분들도 공통화하여 구조적으로 변경해줄 수 있다. 그리고 공통화된 객체를 통해서 View쪽으로 접근하게 하여, Controller에서 직접 View로 이동하는 것이 아니라 Front Controller에서 View로 이동하게 되어 View를 분리할 수 있게 한다. 프로세스 흐름도를 보자.
프로세스 흐름도
패키지 구조
v2 부분의 패키지 구조이다.
V1과 마찬가지로, 인터페이스로 구현할 ControllerV2를 작성한다. 다만 process 메서드의 반환 타입을 MyView로 설정하여 각 Controller가 MyView 객체를 FrontController에게 전달해줄 수 있도록 만든다.
ControllerV2
1
2
3
4
|
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
|
cs |
MyView
반환될 MyView 객체를 생성한다. 이 부분이 viewPath 및 requestDispatcher.forward가 포함된, 공통화된 부분이다. viewPath를 파라미터로 받아 생성자로 주입하고, render() 메서드를 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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);
}
}
|
cs |
각 Controller - MemberSaveControllerV2
각 Controller가 ControllerV2를 상속받도록 하고, 로직 실행 후 MyView에 viewPath를 전달한다. 로직 부분은 같고, viewPath만 각 파일에서 다르게 전달하면 되므로, MemberSaveControllerV2 코드만 대표로 이해하면 된다. MyView 객체를 리턴하는 것을 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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");
}
}
|
cs |
FrontControllerServletV2
마지막으로 프론트 컨트롤러를 작성한다. V1과 코드가 거의 같은데, controller의 process 메서드만 실행하면 끝나는 것이 아니라, process 메서드 이후 받아온 MyView 객체를 통해서 render() 메서드를 실행하여 각 controller에 알맞는 view로 이동하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@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 {
System.out.println("FrontControllerServletV2.service");
String uri = request.getRequestURI();
ControllerV2 controller = controllerMap.get(uri);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView myView = controller.process(request, response);
myView.render(request, response);
}
}
|
cs |
4. Model 추가(V3)
HttpServletRequest와 HttpServletResponse는 필요가 없는 파라미터인데도 불구하고, 서블릿 스펙상 항상 포함되었다. 이제 서블릿 기술에 대한 종속성을 일부 제거하고, Model이라는 객체를 통해 request의 속성들을 받아온다.
그리고, viewPath에서 공통으로 작성된 부분들도 viewResolver라는 객체로 공통화하여 코드 변경이 용이해질 수 있도록 해본다.
V3를 잘 이해하면 다음글에서 다룰 V4, V5는 쉽게 이해된다. 특히 V5는 최종 목표인 스프링 MVC와 같은 구조인데, V5가 V3를 기반으로 조금 변형된 내용이므로 V3를 확실히 이해해야 한다.
프로세스 흐름도
이제 각 Controller에서 MyView를 바로 반환하는 것이 아니라, ModelView를 반환한다. HttpServletRequest, Response 대신 Model 객체를 FrontController에 전달한다.
그 다음 중복된 ViewPath를 제거하는 ViewResolver 처리 후, MyView 객체를 통해 JSP Forward를 수행하게 된다.
공통화되는 부분은 Front Controller가 처리한다는 개념은 동일하다.
v3 구조
구조는 V2와 동일하나, ModelView가 추가되었다. Servlet 대신 이 ModelView를 통해서 FrontController와 각 Controller가 소통한다는 것을 이해해야 한다.
ModelView
ModelView에서는 viewName과 Model이라는 Map 객체를 정의한다. 각 컨트롤러에서 ModelView를 생성할 때, forward 하고자하는 path를 넣기 위해, viewName(=viewPath)을 생성자의 파라미터로 넣어주도록 한다. 그리고 컨트롤러에서 로직을 수행한 객체를 담을 수 있도록, Map 타입으로 model 객체를 만들어놓는다.
1
2
3
4
5
6
7
8
9
10
|
@Getter
@Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
|
cs |
ControllerV3
이제 각 Controller가 ModelView에 저장된 요청 정보인 paramMap을 이용할 수 있도록 파라미터로 지정하고, ModelView 타입을 반환하도록 해준다. V2와 비교해보면, Servlet에 대한 의존성이 제거되었다는 점이 다르다.
1
2
3
4
5
6
7
|
public interface ControllerV3 {
//V2에서의 MyView
// MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
ModelView process(Map<String, String> paramMap);
}
|
cs |
각 컨트롤러
각 컨트롤러는 다음과 같이 작성한다. MemberSaveControllerV3에서는, paramMap에서 클라이언트의 요청정보를 받아와서 memberRepository에 저장한다. 그리고 ModelView에 viewName을 path로 넣고, 생성한 member 객체를 ModelView의 model 객체에 넣어서 반환해준다. 이후 이 model 객체는 FrontController에서 요청 정보 및 viewName(=viewPath)을 찾는데 사용된다. 나머지 컨트롤러도 이와 같은 방식이라고 보면 된다.
MemberSaveControllerV3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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;
}
}
|
cs |
MemberFormControllerV3
1 2 3 4 5 6 |
public class MemberFormControllerV3 implements ControllerV3 { @Override public ModelView process(Map<String, String> paramMap) { return new ModelView("new-form"); } } Colored by Color Scripter |
cs |
MemberListControllerV3
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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;
}
}
|
cs |
FrontController
각 컨트롤러에서 받아온 요청 파라미터 정보를 createParamMap 메서드에서 하나씩 모두 꺼내어 myView에 전달한다. 그리고 컨트롤러에서 받아온 viewName 정보를 기반으로 실제 View(.jsp)로 이동할 절대 경로를 생성하고, 경로를 MyView 객체에 전달한다. 마지막으로 이 객체의 render()메서드를 실행하여 dispatcher.forward 처리를 해주게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@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 {
System.out.println("FrontControllerServletV3.service");
String uri = request.getRequestURI();
ControllerV3 controller = controllerMap.get(uri);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//paramMap에서 파라미터 꺼내서 myView로 전달
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName(); // 논리 이름 new-form
MyView view = viewResolver(viewName);
view.render(mv.getModel(), 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");
}
}
|
cs |
MyView
model 객체에서 요청 파라미터 및 viewName 정보를 실제 View쪽으로 넘겨주는 작업을 한다. model 객체의 파라미터를 하나씩 requestAttribute로 지정해준다. 그리고 viewName 정보를 viewPath로 지정하여 dispatcher.forward로 어떤 View로 이동할지 지정해준다. 이렇게 되면 마지막 View에서는 viewPath가 적용된 .jsp 파일로 이동하고, 사용자의 요청 정보를 request에 들어있는 attribute 정보에서 뽑아내서 렌더링할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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);
}
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
|
cs |
V3 Controller의 흐름을 정리하면 다음과 같다. 프로세스 흐름도와 같은 내용이다.
1. 클라이언트의 요청이 FrontController로 온다(urlPatterns를 지정해놨기 때문)
2. 요청의 uri 정보를 추출하여 알맞는 controller로 매핑한다.
3. 요청의 parameter 정보를 추출하여 각 controller로 보내고, controller에서 save 등 필요한 로직을 처리한다.
4. controller에서 처리된 정보와 viewName을 ModelView 객체 형태로 받아온다.
5. ModelView.viewName을 기반으로 viewResolver로 View의 경로값을 만든다. 그리고 컨트롤러에서 로직이 처리된 후의 파라미터 정보인 ModelView.model를 Myview.render() 메서드에 전달한다.
6. Myview에서는 FrontController에서 받아온 model 정보를 추출하여 request의 attribute로 담고, ViewPath 정보를 통해 알맞는 View(.jsp) 경로로 정보들을 보낸다.
V3의 경우 파일이 많아서 약간 어려울 수 있으나, 계속 보면 익숙해지는 것 같다. 다음 글에서 다룰 V4, V5를 이해하기 위해서 V3를 잘 이해해야 한다.
참조
1. 인프런_스프링 MVC 1편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC] 9. 스프링 MVC - 프로젝트 생성과 로깅 (0) | 2021.08.02 |
---|---|
[스프링 웹MVC] 8. 프론트 컨트롤러(Front Controller) - 2. 컨트롤러 개선, MVC 패턴 요약 (3) | 2021.08.01 |
[스프링 웹MVC] 6. MVC 패턴 적용 (0) | 2021.07.25 |
[스프링 웹MVC] 5. 서블릿, JSP 로 회원관리 웹앱 만들기 (0) | 2021.07.24 |
[스프링 웹MVC] 4. HTTP 응답 정보 : 서블릿 - HttpServletResponse (0) | 2021.07.19 |