이전에 MVC 패턴의 한계에 대해 언급하면서 공통처리가 어렵고 코드 중복이 발생한다는 단점에 대해 논했다.
문제를 해결하기 위해서는 수문장 역할을 하는 프론트 컨트롤러를 ControllerA, ControllerB, ControllerC 앞에 배치하면 된다.
다음과 같은 다이어그램으로 나타난다.
프론트 컨트롤러는 앞에 대표 컨트롤러 하나만 서블릿으로 설정하고
이 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출하는 기능을 한다
모든 요청이 이 프론트 컨트롤러를 거쳐 실제 컨트롤러를 호출함에 따라 실제 ControllerA, ControllerB, ControllerC가 호출되기 이전에 필요한 공통처리 메서드를 실행할 수 있다.
프론트 컨트롤러에서 알아서 찾아서 특정 컨트롤러를 호출할 것이기 때문에 ControllerA, ControllerB, ControllerC는 서블릿을 사용하지 않아도 된다.
WAS Server에서 처음 요청이 들어가는 곳이 servlet이기 때문에 프론트 컨트롤러 빼고는 서블릿이여야 할 이유가 없다.
-> 그렇다면 프론트 컨트롤러가 다른 컨트롤러를 호출하는 과정은 forward와 비슷한 과정이라고 봐도 무방한가?
프론트 컨트롤러 도입 - V1
Interface의 구현을 통해 다형성을 적극 활용하겠다
1) ControllerV1 Interface
package hello.servlet.web.frontcontroller.v1;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
// 이걸 가지고 여러가지, 폼 리스트 저장 컨트롤러로 인터페이스 다 구현할거임.
// 매핑 정보 호출할때 일관성 있게 다형성 이용해서 잘 호출 가능
}
다형성에 관해서 언급했는데
잠시 후 FrontControllerServletV1에서 이 다형성이 어떻게 활용되는지에 대해서 설명하겠다.
2) MemberFormController
package hello.servlet.web.frontcontroller.v1.controller;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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);
}
}
3) MemberSaveControllerV1
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 java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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);
// setAttribute 내에 Map 존재하는데 거기에 들어가게 됨.
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
4) MemberListControllerV1
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 java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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 requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request, response);
}
}
지금까지 구현한 컨트롤러들을 보면 ControllerV1의 구현체라는 것과 서블릿 애노테이션, HttpServlet 상속이 빠졌다는 것을 제외하면
기존의 코드와 거의 동일하다
이 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 java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@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 {
// 요청된 URI에 따라 다른 객체 인스턴스가 반환됨
String requestURI = request.getRequestURI();
ControllerV1 controllerV1 = controllerMap.get(requestURI); // 다형성에 의해 인터페이스를 받는다
// 구체 클래스로 받는 경우, 각 컨트롤러에 따라 다른 타입객체를 명시해야한다.
if(controllerV1 == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controllerV1.process(request, response);
}
}
프론트 컨트롤러 구현 코드를 보자
먼저 이 컨트롤러를 통해 요청이 들어와야하기 때문에 서블릿으로 구현한다
따라서 평소에 해왔듯이 @WebServlet 애노테이션과, HttpServlet을 상속받았다
참고로 urlPatterns에서 * 의 의미는 하위 모든 정보인 경우 다 가져오겠다는 뜻이다
평소에 봐왔던 컨트롤러와 다른점은 Map을 이용해 String, ControllerV1을 담고있다는 점인데
(Key, Value) == (매핑 URL, 호출될 컨트롤러)
그 이유는 컨트롤러에서 조건을 만족하는 컨트롤러를 찾아서 호출해주어야 하기 때문에
어떤 컨트롤러를 어떤 조건에서 호출할 것인가에 대한 정보를 담고 있어야 한다.
생성자에 기본적으로 어떤 컨트롤러를 호출할 것인가에 대해 데이터를 .put 메서드를 이용해 담아준다
그리고 service 메서드 구현을 보자
request.getRequestURI(); 를 이용하여 [localhost:8080/ 이부분] 에 어떤 값이 오는지 확인하고
controllerMap.get(requestURI)를 통해 해당 URI 값에 해당하는 컨트롤러 객체를 호출한다(서블릿 아님)
if문 분기에 경우 해당 URI에 맞는 컨트롤러 객체 호출이 불가능한 경우
response로 SC_NOT_FOUND(=404)를 설정하여 오류 메세지를 띄울 수 있도록 예외처리한 것이며
마지막으로 controllerV1(controllerMap.get()을 통해 찾아낸 컨트롤러 객체)에 process를 통해
request와 response 객체를 넘겨준다.
넘겨주게 되면 MemberFormControllerV1, MemberListControllerV1, MembberSaveControllerV1 중
선택된 컨트롤러는 등록된 viewPath에 존재하는 view(jsp)에
RequestDispatcher를 이용하여 request, response 객체를 전달한다
참고로, 구조를 건들때는 구조만 건드리자
괜히 구조 건드릴때 기능 개선 욕심부렸다가 기능 꼬이면서 온갖것들이 꼬이는 경우가 발생한다
지금 코드들을 보면 viewPath와 RequestDispatcher 그리고 forward 로직 등
여전히 중복이 있고 깔끔하지 않은 부분이 있는데 이후 v2버전에서 이를 개선해보겠다
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 1편' 카테고리의 다른 글
[스프링 웹 MVC 1편] 13. MVC 프레임워크 제작해보기 - v3 (0) | 2023.05.22 |
---|---|
[스프링 웹 MVC 1편] 12. MVC 프레임워크 제작해보기 - v2 (0) | 2023.05.22 |
[스프링 웹 MVC 1편] 10. MVC 패턴 - 한계 (0) | 2023.05.22 |
[스프링 웹 MVC 1편] 9. MVC 패턴 - 개요 (0) | 2023.05.22 |
[스프링 웹 MVC 1편] 8. JSP (0) | 2023.05.21 |