2 분 소요

6.5 MVC 프레임워크 구현 1단계

구현 완료 참고 branch : step2-user-with-mvc-framework

클라이언트 요청에 대한 처리를 담당하는 부분을 Controller interface로 추상화한다.

public interface Controller {
	String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception;
}

지금까지 HttpServlet 을 상속해 구현한 서블릿 코드를 Controller 인터페이스를 구현하도록 변경한다.

예)

as-is

@WebServlet("/users")
public class ListUserController extends HttpServelt {
	private static final long serialVersionUID = 1L;

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		if(!UserSessionUtils.isLogined(req.getSession())) {
			resp.sendRedirect("/users/loginForm");
			return;
		}

		req.setAttribute("users", Database.findAll());

		RequestDispatcher rd = req.getRequestDispatcher("/user/list.jsp");
		rd.forward(req, resp);
	}
}

to-be

public class ListUserController implements Controller {
	@Override
	public String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
		if(!UserSessionUtils.isLogined(req.getSession())) {
			return "redirect:/users/loginForm";
		}

		req.setAttribute("users", Database.findAll());
		return "/user/list.jsp";
	}
}

특별한 로직 없이 뷰(JSP) 에 대한 이동만을 담당하는 ForwardController 를 추가한다.

public class ForwardController implements Controller {

	private String forwardUrl;

	public ForwardController (String forwardUrl) {
		this.forwardUrl = forwardUrl;
		if(forwardUrl == null) {
			throw new NullPointerException("forwardUrl is null.");
		}
	}

	@Override
	public String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
		return forwardUrl;
	}
}

모든 서블릿을 Controller 인터페이스를 구현하도록 변경하는 작업을 완료하면

요청 URL 과 컨트롤러 매핑을 담당하는 작업을 RequestMapping 에 추가한다.

public class RequestMapping {
	private static final Logger logger = LoggerFactory.getLogger(DispatcherServlet.class);
	private Map<String, Controller> mappings = new HashMap<>();

	void initMapping() {
		mappings.put("/", new HomeController());
		mappings.put("/users/form", new ForwardController("/user/form.jsp"));
		mappings.put("/users/loginForm", new ForwardController("/user/login.jsp"));
		mappings.put("/users", new ListUserController());
		mappings.put("/users/login", new LoginController());
		mappings.put("/users/profile", new ProfileController());
		mappings.put("/users/logout", new LogoutController());
		mappings.put("/users/create", new CreateUserController());
		mappings.put("/users/updateForm", new UpdateFormUserController());
		mappings.put("/users/update", new UpdateUserController());

		logger.info("Initialized Request Mapping.");
	}

	public Controller findController(String url){
		return mappings.get(url);
	}

	void put(String url, Controller controller) {
		mappings.put(url, controller);
	}
}

RequestMapping 은 서비스에서 발생하는 모든 요청 URL 과 각 URL 에 대한 서비스를 담당할 컨트롤러를 연결하는 작업을 한다.

클라이언트의 모든 요청을 받아 URL 에 해당하는 컨트롤러로 작업을 위임하고, 실행된 결과 페이지로 이동하는 작업을 담당할 DispatcherServlet 을 구현한다.

@WebServlet(name = "dispatcher", urlPatterns = "/", loadOnStartup = 1)
public class DispatcherServlet extends HttpServelt {
	private static final long serialVersionUID = 1L;
	private static final Logger logger = LoggerFactory.getLogger(DispatcherServlet.class);
	private static final String DEFAULT_REDIRECT_PREFIX = "redirect:";

	private RequestMapping  rm;

	@Override
	public void init() throws ServletException {
		rm = new RequestMapping();
		rm.initMapping();
	}

	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		String requestUri = req.getRequestURI();
		logger.debug("Method : {}, Request URI : {}", req.getMethod(), requestUri);
		
		Controller controller = rm.findController(requestUri);
		try {
			String viewName = controller.execute(req, resp);
			move(viewName, req, resp);
		} catch (Throwable e) {
			logger.error("Exception : {}", e);
			throw new ServletException(e.getMessage());
		}
	}

	private void move(String viewName, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		if(viewName.startsWith(DEFAULT_REDIRECT_PREFIX)){
			resp.sendRedirect(viewName.substring(DEFAULT_REDIRECT_PREFIX.length()));
			return;
		}
		
		RequestDispatcher rd = req.getRequestDispatcher(viewName);
		rd.forward(req, resp);
	}		
}

정적 자원 처리는 DispatcherServlet 으로 요청되기 이전에

core.web.filter.ResourceFilter 에서 “default” 서블릿이 처리하도록 구현하고 있다.

HomeController 에서 “/”로 매핑하여 접근 시, webapp 디렉토리에 index.jsp 가 존재한다면 HomeController 가 아닌 index.jsp 로 요청이 처리된다는 점을 주의해야한다. 이렇게 처리되는 이유는 path 가 없는 경우 (index.jsp 가?) 처리를 담당하는 기본 파일로 설정되어 있기 때문이다.

따라서 HomeController 에서 이동할 뷰를 지정할 때 index.jsp 가 아니라 home.jsp 로 사용했다.

서블릿을 매핑할 때 사용하는 loadOnStartup 설정은 서블릿의 인스턴스를 생성하는 시점과 초기화를 담당하는 init() 메소드를 어느 시점에 호출할 것인가를 결정하는 설정이다.

loadOnStartup 을 설정하지 않으면 서블릿 컨테이너가 시작을 완료한 후 클라이언트의 요청이 최초로 발생하는 시점에 서블릿 인스턴스 생성과 초기화가 이루어진다.

loadOnStartup 을 설정하면 서블릿 컨테이너가 시작하는 시점에 서블릿 인스턴스 생성과 초기화가 이루어진다.

loadOnStartup 숫자 값은 여러 개의 서블릿의 초기화 순서를 결정하며 숫자가 낮은 순으로 우선순위를 갖는다.