* 이 포스팅은 부산대학교 2023 백엔드 미니 부트캠프 2주차 학습 내용을 정리한 글입니다.
이전 포스팅을 간략하게 요약하자면 다음과 같다.
클라이언트의 요청으로 아파치 등 웹 서버로 1차 처리를 거친 후 톰캣 등 WAS로 서블릿 객체를 만들어 요청과 응답을 처리한다.
간단하게 생각하면 엔드포인트에 따라 요청을 분리할 수 있다.
하지만 위와 같은 모델의 문제점은 단일 진입점이 없어 클라이언트 요청을 1차적으로 처리할 수 없다. 그래서 서블릿 객체에 대해 공통 처리를 담당하고 그에 따른 요청을 분기하는 모델을 다음과 같이 생각할 수 있다.
위와 같은 디자인 패턴을 프론트 컨트롤러 패턴이라고 한다. HttpServlet
을 상속한 FrontController
객체를 만들면 다음과 같다.
// import 문 생략
@WebServlet("*.do")
public class FrontController extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String action = req.getRequestURI();
if (action.equals("/board.do")) {
resp.sendRedirect("board.jsp");
} else if (action.equals("/user.do")) {
resp.sendRedirect("user.jsp");
} else {
// 예외 응답 처리 로직
}
}
}
위 코드는 req
의 uri 엔드포인트에 따라 jsp
파일로 리다이렉션하는 코드이다. uri와 url의 차이점은 이 글에서 잘 정리돼 있다.
하지만 위 코드는 다음과 같은 문제점이 있다.
jsp
를 직접 호출할 수 있다. → 서버 리소스에 직접 접근하는 방식은 보안에 취약하다.jsp
파일에서 쓸데 없이 자바 코드를 작성해야 한다. 이는 유지 보수에 좋지 않다.FrontController
에 요청한 뒤, View
에 재요청을 하는 경우, request 데이터가 소실된다. HTTP 프로토콜은 request가 처리된 뒤 사라진다. 세션에 request 데이터를 저장하는 방식은 지연 시간을 늘릴 수 있다.스프링에서는 DispatcherServlet
이라는 객체가 프론트 컨트롤러 역할을 한다. 하지만 DispatcherServlet
객체를 개발자가 직접 조작하는 경우는 흔하지 않다. 어노테이션 없는 하드 코딩에서는 다음과 같이 엔드포인트를 라우팅할 수 있다.
req.getRequestDispatcher("/WEB-INF/views/user.jsp").forward(req, resp);
위 코드에서 req.getRequestDispatcher(_PATH)
는 req
의 RequestDispatcher
객체를 가져온다. 그리고 RequestDispatcher
객체를 _PATH
경로로 요청을 전달할 수 있다. 이렇게 전달된 요청은 다시 .forward(req, resp)
메서드로 _PATH
경로의 리소스로 전달되어 클라이언트에게 응답을 보낸다.
/WEB-INF/views
디렉토리에 jsp
를 숨기는 경우 외부 접근이 불가능해진다. 또한 RequestDispatcher
로 Tomcat에 의해서 내부 요청으로 접근할 수 있다.
하지만 이런 프론트 컨트롤러 패턴은 프로젝트 규모가 커질 경우 그 한계가 명확하게 드러난다.
이런 문제를 해결하기 위해 컴포넌트를 분리하고 역할과 책임을 분리하여 디자인할 수 있다.
서버 컴포넌트를 역할과 책임에 따라 분리하는 대표적인 디자인 패턴은 MVC 패턴이다. MVC 패턴은 3가지 컴포넌트로 나뉜다.
이런 MVC 패턴도 크게 2가지 단점이 있다.
그렇다면 위 두 가지를 합치면 적절한 trade-off 지점을 찾을 수 있다.
프론트 컨트롤러와 MVC 패턴/구조를 합치면 요청의 엔드포인트에 따라 컴포넌트가 달라질 수 있다. 즉, 한 어플리케이션에서 여러가지 MVC 구조가 하부 구조로 구성될 수 있다는 말이다. 보통 DB 테이블 마다 MVC가 생긴다.
클라이언트 요청을 최초로 프론트 컨트롤러가 처리하고 해당하는 적절한 컨트롤러(이 경우 MVC 구조의 컨트롤러)를 요청한다. 컨트롤러가 데이터를 요청하면 모델이 DB에서 데이터를 request에 저장한다. 이때 request 데이터를 유지하려면 RequestDispatcher
를 사용하여 유지해야 한다. 사용자에게 응답 시 View를 찾아 응답한다. 데이터 요청 없이 단순한 정적 페이지를 요청하는 경우 바로 View 응답을 보내면 되지만, 데이터를 담아 응답하는 경우 RequestDispatcher
에 담긴 데이터를 꺼내 응답한다.
실제 스프링 프로젝트에서 프론트 컨트롤러를 구현하는 경우는 매우 드물다. DispatcherServlet
객체가 자동 생성돼 모든 요청을 중앙에서 1차적으로 처리한다. 개발자는 요청의 전/후처리 및 예외를 일관되게 관리할 수 있다.
쓰다보니 포스팅이 길어졌다. 다음 포스팅에서 리플렉션과 어노테이션, 그리고 이들이 스프링에서 사용되는 방법을 설명한다.