프론트 컨트롤러란?
- 서블릿 하나로 클라이언트의 요청을 다 받아서 공통 처리를 중복없이 하기 위해 사용된다.
- 이후 나머지 컨트롤러들은 서블릿이 사용하지 않는다.
프론트 컨트롤러가 요청을 받고 응답하기 때문이다. 즉, 프론트 컨트롤러가 받아서 다른 컨트롤러를 사용하기 때문이다. - 스프링도 DispatcherServlet이 FrontController이다.
기존의 패턴을 설명하자면 아래 그림과 같다
각 클라이언트들은 Controller A, B, C에 대해 각각 호출한다.
공통 코드들은 별도로 처리되어 있지 않고 각 Controller에 포함되어 있다.
하지만 프론트 컨트롤러 패턴을 도입한다면?
각 클라이언트들은 Front Controller에 요청을 보내고, Front Controller은 각 요청에 맞는 컨트롤러를 찾아서 호출시킨다.
공통 코드에 대해서는 Front Controller에서 처리하고, 서로 다른 코드들만 각 Controller에서 처리할 수 있도록 한다.
장점을 정리해보자면,
- 공통 코드 처리가 가능
- Front Controller 외 다른 Controller에서 Servlet 사용하지 않아도 됨
스프링 웹 MVC의 핵심도 위 같은 FrontController이다.
(스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있음)
구조 살펴 보기
구조를 살펴보면 아래와 같다.
- 프론트 컨트롤러에 요청이 온다.
→ 프론트 컨트롤러는 HttpServlet을 상속 받는다. - 프론트 컨트롤러는 컨트롤러들의 URL 관련 정보들을 갖고 있는데 여기서 조회를 한다.
→ Interface에 컨트롤러들을 Map으로 갖고 있다. - 매핑된 컨트롤러를 호출한다.
- 매핑된 컨트롤러는 공통 로직 이외의 본인의 역할을 수행한다.
이제 간단한 예제를 통해 Front Controller에 대해 파악해보자.
ControllerV1
public interface ControllerV1 {
void process(HttpServletRequest req, HttpServletResponse res) throws ServletException,IOException;
}
MemberFormControllerV1
public class MemberFormControllerV1 implements ControllerV1{
@Override
public void process(HttpServletRequest req, HttpServletResponse res) throws ServletException,IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, res);
}
}
MemberSaveControllerV1
public class MemberSaveControllerV1 implements ControllerV1{
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String username = req.getParameter("username");
int age = Integer.parseInt(req.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
req.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, res);
}
}
MemberListControllerV1
public class MemberListControllerV1 implements ControllerV1{
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
req.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, res);
}
}
MemberFormControllerV1, MemberSaveControllerV1, MemberListControllerV1는 ControllerV1 인터페이스를 구현한 구체 컨트롤러다.
이제 가장 중요한 Front Controller를 생성해보자.
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 req, HttpServletResponse res) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String reqURI = req.getRequestURI();
ControllerV1 controller = controllerMap.get(reqURI);
if(controller == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(req, res);
}
}
- urlPatterns = "/front-controller/v1/*": front-controller/v1 하위의 모든 항목에 대해 해당 서블릿 실행
- 생성자: 각 url에 대해 Controller를 매핑하기 위해 Map에 데이터를 put
- request에 대한 URI 값을 가져와서 controllerMap에서 어떤 컨트롤러에 매핑 되었는지 찾는다
- 만약 controller가 존재하지 않는다면 404 에러 처리
이제 위에서 살펴봤던 그림을 예제 코드를 짚어가며 다시 보자
각 클라이언트들은 /front-controller/v1 하위의 어떤 경로를 접속하던간에 Front Controller으로 이동한다.
예를 들어, localhost:8080/front-controller/v1/members에 접속했다고 가정하자.
- @WebServlet의 urlPatterns에 의해 FrontControllerServletV1으로 이동
- 접속한 URI를 받고, ControllerV1 controller 변수에 MemberListControllerV1 저장 (Map에서 꺼내옴)
- MemberListControllerV1의 process 실행
위 예제는 아직 모든 Controller에서 RequestDispatcher을 이용해 view로 이동한다는 코드가 계속 반복되고 있다.
하지만 모든 요청이 Front Contoller를 통해 구체적인 컨트롤러들이 실행된다는 것은 파악할 수 있을 것이다.
스프링는 프론트 컨트롤러 패턴을 따르고 이를 DispatcherServlet이 담당한다.
DispatcherServlet은 클라이언트의 요청을 먼저 받아 필요한 처리를 한 뒤, 개발자가 구현한 요청에 맞는 핸들러에게 요청을 Dispatch하고 해당 핸들러의 실행 결과를 Response형태로 만드는 역할을 한다.
참고