Front Controller 패턴이란?
Front Controller 패턴은 모든 요청을 하나의 Controller 에서 받고, 내부적으로 적절한 컨트롤러에 부여하는 방식이다. 주로 MVC 패턴에서 많이 사용된다.
결론부터 말하자면, 위 패턴을 사용한 이유는 처음 구현 시 서블릿마다 중복되는 코드가 많았다. url 마다 각각의 서블릿이 필요했고, 각 서블릿 끼리 중복되는 작업이 많아, 하나로 합치면 좋겠다 생각하여 리팩토링을 시도했다.
+ 추가로, Front Controller 패턴은 중복 코드 제거 말고도, Command 인터페이스를 구현함으로써 서블릿 API의 의존도를 줄일 수 있다. -> 이는 POJO 기반 구조로 전환하여 코드의 유연성과 확장성을 확보하는데 용이하다.
Servlet 을 이용해 간단한 CRUD 프로젝트를 만들어보며, 기본적인 서블릿의 동작 원리와 구조를 이해할 수 있었다.
전체적인 구조는 view를 JSP를 활용해 구현하고, 아래와 같이 각 기능 별 서블릿을 생성 후 매핑하여 페이지를 띄우거나, 리다이렉트하는 방식이었다.

이때 데이터 저장은 Repository를 Map 방식으로 저장하여 서블릿 컨텍스트에 등록 후, 각 서블릿에서 접근하도록 했다. (실무에서는 대충 아래와 같은 이유들로 Map 방식을 사용하지도, 사용해서도 안되지만 학습 단계이니 하나씩 리팩토링해나가려 한다.)
- Map 은 메모리 영역에 저장되므로 휘발성 데이터이다.
- 일반 HashMap 은 멀티스레드 영역에서 동시성 문제가 발생한다.
중복되는 코드 부분 제거
// RegisterServlet 일부
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("action", "/student/register");
RequestDispatcher rd = getServletContext().getRequestDispatcher("/WEB-INF/student/register.jsp");
rd.forward(req, resp);
}
// ListServlet 일부
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// student List 구하기
List<Student> studentList = studentRepository.getStudents();
req.setAttribute("studentList", studentList);
// /student/list.jsp <- forward 하기
RequestDispatcher rd = getServletContext().getRequestDispatcher("/WEB-INF/student/list.jsp");
rd.forward(req, resp);
}
우선 다시 돌아와서, 위의 구조를 보면 CRUD 기능에는 문제가 없지만, 각 url 마다 각각의 서블릿이 필요했고, 중복되는 코드가 많았다.
대표적으로 RequestDispatcher 부분이 있었는데, 각 요청과 응답을 동적으로 전달하기 위해선 필수적으로 사용해야 했다. 따라서, 해당 부분을 Front Controller 에서 처리하면 코드의 중복을 상당히 줄일 수 있을 것 같았다.
리팩토링 방식
1. Front Controller 생성
// Front Servlet 일부
@WebServlet(name = "frontServlet", urlPatterns = "*.do")
public class FrontServlet extends HttpServlet {
private static final String REDIRECT_PREFIX = "redirect:";
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 공통 처리 - 응답 content-type, character encoding 지정.
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
try {
각 서블릿에 붙였던 어노테이션을 Front Controller(Servlet) 에만 붙인다. 이때, url은 "*.do" 로 설정하는데, 이는 기존의 url을 각각 입력받는 방식에서, 서블릿으로 동적 생성해야하는 페이지는 ".do" 를 붙임으로써 서블릿에서 처리가 필요한 요청들은 해당 Front Controller 에서 우선적으로 받게된다.
2. URI -> Command 매핑
public interface Command {
String execute(HttpServletRequest req, HttpServletResponse resp);
}
지금 현재 코드는 각 서블릿이 @WebServlet 어노테이션 즉, 서블릿 API 을 사용하고 있다. 그러나, 이를 각 URI따라 처리할 객체를 순수 자바 (POJO) 방식으로 구현할 수 있다.
위에 보이는 Command 인터페이스를 구현하는 방식으로 작성하면 되는데, 구현해야할. execute 메서드를 보면 Servlet 의. service 메서드와 닮은 것을 볼 수 있다. 이를 구현하면, 아래처럼 Servlet API를 사용하지 않고도 해당 URI 의 요청과 응답을 처리할 수 있게 되어, 서블릿의 의존도를 낮춘다고 표현한 것이다.
public class StudentListController implements Command {
@Override
public String execute(HttpServletRequest req, HttpServletResponse resp) {
StudentRepository studentRepository = (StudentRepository) req.getServletContext().getAttribute("studentRepository");
List<Student> studentList = studentRepository.getStudents();
req.setAttribute("studentList", studentList);
return "/WEB-INF/student/list.jsp";
}
}
최종적으로 Front Controller 에서 아래 처럼 각 URI 에 맞는 Controller를 매핑할 수 있게 된다.
Command command = resolveCommand(servletPath, method);
String view = command.execute(req, resp);
private Command resolveCommand(String servletPath, String method) {
Command command = null;
if("/student/list.do".equals(servletPath) && "GET".equalsIgnoreCase(method) ){
command = new StudentListController();
}else if("/student/view.do".equals(servletPath) && "GET".equalsIgnoreCase(method) ){
command = new StudentViewController();
}else if("/student/delete.do".equals(servletPath) && "POST".equalsIgnoreCase(method) ){
command = new StudentDeleteController();
}else if("/student/update.do".equals(servletPath) && "GET".equalsIgnoreCase(method) ){
command = new StudentUpdateFormController();
}else if("/student/update.do".equals(servletPath) && "POST".equalsIgnoreCase(method) ){
command = new StudentUpdateController();
}else if("/student/register.do".equals(servletPath) && "GET".equalsIgnoreCase(method) ){
command = new StudentRegisterFormController();
}else if("/student/register.do".equals(servletPath) && "POST".equalsIgnoreCase(method) ){
command = new StudentRegisterController();
}else if("/error.do".equals(servletPath)){
command = new ErrorController();
}
return command;
}
'서블릿(Servlet)' 카테고리의 다른 글
| [Web] Redirect vs Forward 차이점 정리 (0) | 2025.04.22 |
|---|---|
| [Web] 서블릿(Servlet) 기본 구조 정리 (1) | 2025.04.21 |