강의 내용이 있기 때문에 출처를 밝힙니다 !
임의로 추가, 수정, 삭제한 내용들이 있으니 정확한 이해를 위해서는 강의를 구매하시는 것을 추천드립니다 😅
출처 : 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (인프런 강의)
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의
웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -
www.inflearn.com
기존 MVC 패턴의 한계
기존 서블릿 클래스들을 한번 다시 살펴보자.
어떤 요청이 오냐에 따라 다른 로직도 물론 있지만, 중복되는 곳이 심심찮게 보인다.
(세 클래스 모두 HttpServelt 클래스를 상속받고 있고, viewPath 부분이나 forward() 호출 부분 등)
이 구조로 계속해서 개발을 이어나가게 되면 비슷한 서블릿 클래스가 많이 생길텐데 .. 어떻게 구조를 개선할 수 있을까 ?
1. 기존 회원가입 폼 클래스
@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
2. 기존 회원목록 클래스
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
private final MemberRepository repository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = repository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
3. 기존 회원가입 클래스
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
private final MemberRepository repository = MemberRepository.getInstance();
@Override
protected void service(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);
repository.save(member);
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
그림으로 확인하는 기존 MVC 구조와 앞으로 도입할 FrontController 도입 이후 구조
1. FrontController 도입
클라이언트 요청이 올 때, 맨 앞에서 서블릿의 모든 공통부분을 담당하는 FrontController 패턴을 도입해보자.
FrontController 만 HttpServelt 클래스를 상속받도록 하고, 나머지 컨트롤러들은 자신의 역할에 알맞은 로직만을 수행하도록 하면 된다.
HashMap의 Key 는 클라이언트의 요청 URL, Value 는 요청 URL 에 맞는 알맞은 컨트롤러 객체를 생성하도록 선언한다.
@WebServlet(name="frontControllerServletV1", urlPatterns="/front-controller/v1/*")
public class FrontControllerV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/frontController/v1/members/new-form", new MemberFormControllerV1());
controllerMap.put("/frontController/v1/members/save", new MemberSaveControllerV1());
controllerMap.put("/frontController/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws SevletExcetpion, IOExcetpion {
... 내부 구현은 여기서 ...
}
}
1-1. 각 Controller 클래스가 상속해야하는 ControllerV1 인터페이스 생성
MemberFormControllerV1, MemberSaveControllerV1, MemberListControllerV1 의 세 가지 컨트롤러는 이 ControllerV1
인터페이스의 process() 메서드를 구현해서 자신들만의 로직을 써내려갈 수 있도록 해준다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
1-2. service() 메서드 구현
HttpServlet 클래스를 상속받았기 때문에, service() 메서드를 구현해야 하는데, 여기서 FrontController 의 핵심 가치가 나타난다.
원래는 하나의 클래스가 서블릿 클래스가 되어 URL 패턴을 가지고 있고, 클라이언트 요청이 올 때 해당 클래스가 호출되는 식이였는데,
아래 예시 처럼 요청 URL 을 받아와 알맞은 컨트롤러를 찾아 호출하게 하면, 굳이 여러 개의 서블릿 클래스를 만들 필요가 없어지는 것이다.
아래 코드를 예시로 설명하자면,
1. "/front-controller/v1/*" 이후 경로로 어떤 경로가 오던 이 FrontController 서블릿 클래스가 호출되게 된다. ("*" 덕분에 ㅎㅎ)
2. 호출되면서 생성자를 통해 폼, 회원가입, 회원목록 컨트롤러를 Map 에 담아준다.
3. request.getRequestURI() 메서드를 통해 클라이언트에서 요청한 URI 를 저장한다.
4. 저장된 URI 를 통해 알맞은 컨트롤러를 Map 에서 찾아내고, null 체크를 한다.
5. null 이 아니라면 찾아낸 컨트롤러의 process() 메서드의 인자로 request, response 를 담은 뒤 process() 메서드를 호출한다.
@WebServlet(name="frontControllerServletV1", urlPatterns="/front-controller/v1/*")
public class FrontControllerV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/frontController/v1/members/new-form", new MemberFormControllerV1());
controllerMap.put("/frontController/v1/members/save", new MemberSaveControllerV1());
controllerMap.put("/frontController/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws SevletExcetpion, IOExcetpion {
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response)
}
}
2. 회원가입 폼 컨트롤러 생성
코드에서 알 수 있듯, 이제 이런 컨트롤러 클래스들은 더 이상 서블릿 클래스가 아니므로, 서블릿 컨테이너에서 관리하지 않는다.
viewPath 를 지정하고 dispatcher, forward() 를 통해 JSP 를 호출하는 방식은 이전과 동일하다.
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletExcetpion, IOExcetpion {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getReqiestDispathcer(viewPath);
dispathcer.forward(request, response);
}
}
하지만 이걸로는 부족하다. viewPath, RequestDispatcher 부분 또한 모든 컨트롤러에서 사용되고 있으므로 분리해야 한다.
다음 포스팅에서 해당 공통부분을 더 추출해보도록 하자.
'Servlet' 카테고리의 다른 글
FrontController 를 통한 MVC 패턴 구조 개선 [2] (0) | 2022.04.03 |
---|---|
Servlet 에 MVC 패턴 도입하기 (0) | 2022.04.03 |
[POST] 클라이언트 요청 Body 를 서버로 전달하기 (0) | 2022.03.30 |
[GET] 클라이언트 요청 쿼리 파라미터를 서버로 전달하기 (0) | 2022.03.30 |