본문 바로가기

서블릿(Servlet)

[Servlet] Front Controller 패턴 적용

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;
}