내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
프론트 컨트롤러 패턴에서 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다!
1. 프론트 컨트롤러 도입 - v1
우선 모든 컨트롤러들이 공통적으로 구현해야하는 ControllerV1 이라는 interface를 구현해 보자. 코드는 다음과 같다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
간단하게 porcess()라는 메서드만 선언하고 있다.
이후 Controller1을 구현하는 클래스 들에서는 모두 process() 메서드를 구현해야 한다.
프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
이제 인터페이스를 구현한 컨트롤러를 만들어 보자.
● MemberFormControllerV1 - 회원 등록 컨트롤러
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);
}
}
● 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);
System.out.println("member = " + member);
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);
}
}
● 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);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
내부 로직은 거의 기존의 코드와 동일하다. 달라진점은 HttpServlet 을 상속하지 않은점과, 위에 @WebServlet 애노테이션을 적용하지 않았다는 점 이다.
이제 프론트 컨트롤러를 만들어보자.
● 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 {
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if(controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
프론트 컨트롤러의 urlPatterns를 보면 "/front-controller/v1/*" 이라고 되어있다.
이는 /front-controller/v1/을 포함한 하위의 어떠한 요청이든 프론트컨트롤러에서 받을수 있다는 의미이다.
controllerMap을 통하여 URL 정보가 들어오면 알맞은 controller를 찾아서 반환해준다.
- key: 매핑 URL
- value: 호출될 컨트롤러
service()
먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾는다.
만약 없다면 404(SC_NOT_FOUND) 상태 코드를 반환한다.
컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다.
실행해 보면 기존의 MVC와 동일하게 작동하는것을 확인할 수 있다.
2. View 분리 - v2
모든 컨트롤러마다 뷰로 이동하는 부분에 중복이 많고, 깔끔하지가 않다.
viewPath적고, dispatcher 만들고, forward 하는 부분이 컨트롤러 마다 중복되어 있다.
이를 해결하기 위해 별도로 뷰를 처리하는 객체를 만들자!
기존에는 controller에서 JSP로 바로 forward 했지만, 이번에는 Controller는 MyView를 반환하고 이를 Front Controller가 render()를 호출하여 JSP를 호출하도록 만들었다. 코드로 살펴보자.
● 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);
}
}
MyView는 생성할때 인자로 viewPath를 전달받는다.
또한 render라는 메서드를 갖고 있는데, 이 메서드를 호출시 생성자의 인자로 받은 viewPath로 forward한다.
이제 새로운 컨트롤러의 인터페이스를 만들어보자.
● ControllerV2
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
V1과 달라진점은 MyView 객체를 반환한다는 점 이다.
이제 Form, Save, List 컨트롤러를 만들어 보자.
● 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");
}
}
이제 각 controller들은 viewPath 기록, dispatcher 만들고, forward 하는 복잡한 과정을 직접 수행하지 않는다.
단순하게 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 된다.
● 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);
System.out.println("member = " + member);
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);
}
}
process의 반환 타입이 MyView 이므로 프론트 컨트롤러는 컨트롤러의 process 호출 결과로 MyView 를 반환 받는다.
그리고 view.render() 를 호출하면 forward 로직을 수행해서 JSP가 실행된다.
프론트 컨트롤러의 도입으로 MyView 객체의 render()를 호출하는 부분을 모두 일관되게 처리할 수 있다.
● 실행
신규 등록을 위해 http://localhost:8080/front-controller/v2/members/new-form 로 접속해 보자.
처음 요청이 들어왔을때 이를 urlPatterns = /front-controller/v2/* 를 갖는 프론트 컨트롤러가 받게 된다.
해당 URI를 request.getRequestURI()로 추출하고, 이를 이용하여 알맞은 controller를 찾게된다.
찾은 컨트롤러에서 process() 메서드를 호출하면, "/WEB-INF/views/new-form.jsp" 를 갖고있는 MyView 객체가 반환된다.
이후 반환 받은 MyView 객체에 request, response 인자를 전달하여 render() 메서드를 호출하면
MyView가 갖고있던 viewPath로 forward하게된다.
결론적으로 다음 화면이 클라이언트에게 보여진다. 우리가 요청한 new-form 에 해당되는 jsp가 출력된 것 이다.
username 과 age 를 입력한 후, 전송 버튼을 누르면 서버측으로 HTTP 메시지가 전송된다.
3. Model 추가 - v3
● 서블릿 종속성 제거
컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 꼭 필요할까?
요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.
즉, controller를 호출할때 인자로 request, response 객체를 넘길 이유가 없는 것 이다.
그리고 request 객체를 데이터를 저장하는 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다.
우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 v3를 만들어보자!
이렇게 하면 구현 코드도 매우 단순해지고, 테스트 코드 작성이 쉽다.
● 뷰 이름 중복 제거
기존의 컨트롤러v2 에서는 뷰 이름을 절대경로로 다 지정해서 MyView 객체에 담아서 반환하였다.
하지만 뷰 이름에도 중복되는 부분이 많다. 중복되는 부분을 제거하고, 대신 컨트롤러는 뷰의 논리 이름을 반환하도록 구현해보자.
실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 구현하자.
예를 들면 다음과 같다.
/WEB-INF/views/new-form.jsp => new-form 이 논리 이름
/WEB-INF/views/save-result.jsp => save-result 이 논리 이름
/WEB-INF/views/members.jsp => members 이 논리 이름
우선 V3의 전체 구조를 살펴보자.
새롭게 보이는 ModelView, viewResolver 가 있다.
● ModelView
지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다.
그리고 Model도 request.setAttribute() 를 통해 데이터를 저장하고 뷰에 전달했다.
서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체를 만들어보자.
(이번 버전에서는 컨트롤러에서 HttpServletRequest를 사용할 수 없다. 따라서 직접 request.setAttribute() 를 호출할 수 도 없다. 따라서 Model이 별도로 필요하다.)
● 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;
}
}
ModelView 객체는 렌더링할때 필요한 model 객체와 view의 논리적 이름을 갖고있다.
model은 단순하게 map으로 구현하였으며, 컨트롤러에서 JSP view에서 사용할 데이터를 key : value 쌍으로 넣어준다.
● ControllerV3
public interface ControllerV3 {
ModelView process(Map<String, String> paraMap);
}
이번 V3 컨트롤러는 서블릿 기술을 전혀 사용하지 않는다. 따라서 테스트 코드 작성시 유리해진다.
서블릿 기술은 사용하지 않되, HttpServletRequest가 제공하는 파라미터 데이터들은 Front Controller가 paraMap에 Map 형식으로 담아서 전달해준다.
process 함수의 반환값이 ModelView 인데, 결과적으로 컨트롤러로부터 뷰 이름 + 뷰에 전달할 Model = ModelView 객체를 반환받게 된다.
● MemberFormControllerV3 - 회원 등록 폼
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paraMap) {
return new ModelView("new-form");
}
}
ModelView 를 생성할 때 new-form 이라는 view의 논리적인 이름을 지정한다. 실제 물리적인 이름은 프론트 컨트롤러에서 처리한다.
● MemberSaveControllerV3 - 회원 저장
public class MemberSaveControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paraMap) {
String username = paraMap.get("username");
int age = Integer.parseInt(paraMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
paramMap.get("username"); => 파라미터 정보는 map에 담겨있다. map에서 필요한 요청 파라미터를 조회하면 된다.
mv.getModel().put("member", member); => 모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 담고 반환한다.
● MemberListControllerV3 - 회원 목록
public class MemberListControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paraMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}
● FrontControllerServletV3
@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;
}
// paraMap
Map<String, String> paraMap = createParamMap(request);
ModelView mv = controller.process(paraMap);
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");
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paraName -> paraMap.put(paraName, request.getParameter(paraName)));
return paraMap;
}
}
우선 createParamMap()메서드와 viewResolver()메서드에 대하여 알아보자.
1) createParamMap()
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paraName -> paraMap.put(paraName, request.getParameter(paraName)));
return paraMap;
}
frontCotroller는 서블릿 request에 담겨온 파라미터 정보를 모두 추출하여 Map 에 저장해서 개별 controller들에게 전달해야한다.
따라서 인자로 request를 전달하면 데이터를 추출하여 Map으로 반환해주는 함수를 만든 것 이다.
2) viewResolver()
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
컨트롤러가 반환한 논리 뷰 이름을 frontController는 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리경로가 있는 MyView 객체를 반환한다.
논리 뷰 이름: members
물리 뷰 경로: /WEB-INF/views/members.jsp
컨트롤러 로부터 ModelView를 전달받았을때 여기에 논리 뷰 이름이 포함되어 있다.
따라서 이 논리 뷰 이름을 추출하여 => 물리적 뷰 경로로 변경해줘야 한다. frontCotroller에서는 다음 코드 부분에서 진행된다.
ModelView mv = controller.process(paraMap); // ModleView 객체를 반환 받음
String viewName = mv.getViewName(); // 논리이름 추출
MyView view = viewResolver(viewName); // 논리이름을 통한 MyView 객체 반환
view.render(mv.getModel(), request, response);
MyView 객체를 통해서 HTML 화면을 렌더링 한다.
뷰 객체의 render() 는 모델 정보도 함께 받는다.
JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다.
JSP로 포워드 해서 JSP를 렌더링 한다.
● 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);
}
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));
}
}
'BackEnd > Spring MVC' 카테고리의 다른 글
[Spring] 스프링 MVC - 구조 이해 (0) | 2022.02.25 |
---|---|
[Spring] MVC 프레임워크 만들기 - 2 (0) | 2022.02.23 |
[Spring] 서블릿, JSP, MVC 패턴 (0) | 2022.02.17 |
[Spring] 서블릿 - 2 (0) | 2022.02.14 |
[Spring] 서블릿 - 1 (0) | 2022.02.13 |
댓글