이번 시간에는 Spring Web MVC의 기반이 되는 Servlet의 개념과 Spring Web MVC의 핵심인 DispatcherServlet에 대해 알아보겠습니다. 김영한님의 스프링 MVC 1편과 다른 여러 자료들을 참고하였습니다.
서블릿(Servlet)에 대해서는 이전 포스트에서 간단하게 한 번 다룬 적이 있지만, 중요한 개념이기 때문에 다시 한 번 정리해보겠습니다.
서블릿은 자바 클래스입니다. 서블릿은 하나의 인스턴스이며, 하나의 요청을 처리하는 스레드입니다.
HTTP 요청이 들어오면, WAS(Web Application Server)는 다음과 같은 동작 과정을 거칩니다. WAS에 대한 추가적인 설명은 이전 포스트에서 확인하실 수 있습니다. 시간이 없으신 분들을 위해 WAS란 Spring등 애플리케이션 코드가 돌아가는 서버라고 이해해주시면 되겠습니다.
init()
메서드를 호출해 초기화합니다.service()
메서드를 호출하면서 Request와 Response를 만들어 넘겨줍니다.아래는 직접 확인해볼 수 있는 Java 패키지 안에 Servlet 클래스의 일부입니다. 코드를 보시기 보다는 '클래스구나' 하는 감을 잡으시기 좋을 것 같아 첨부해두었습니다.
백엔드 서버 로직을 짠다는 것(백엔드 개발을 한다는 것)은 정적 페이지만으로 제공할 수 없는 데이터를 동적 페이지로(REST라면 데이터만) 만들어 제공하겠다는 것을 의미합니다.
서버는 각 요청을 '해석'하고 요청 데이터에 맞게 코드를 실행시켜야 합니다. 아무쪼록 코드가 실행되야 데이터를 DB에서 가져와서 페이지로 만들어 반환해줄 수 있습니다. 이러한 코드가 서블릿의 service()
메서드 안에서 실행됩니다. 코드가 요청에 담긴 자료를 확인하고, 필요한 응답을 담을 수 있도록 Request와 Response 객체를 service()
메서드로 넘겨줍니다. 개발자가 HTTP 요청의 해석과 반환 과정을 신경 쓸 필요 없도록 해당 과정을 추상화한 것이 서블릿 클래스인 것입니다. 실행할 비즈니스 로직만 작성하면 나머지 작업은 WAS가 처리하도록 역할을 나누어놨습니다.
위 과정을 보면 WAS가 서블릿이라는 클래스를 인스턴스로 만들어서 호출하고 서블릿의 생명주기를 관리하는 것을 알 수 있습니다. 이런 역할을 하는 WAS의 기능을 서블릿 컨테이너(Servlet Container)라고 부릅니다. WAS가 서블릿 컨테이너보다는 조금 더 넓은 범위지만, 일반적으로 서블릿 컨테이너 기능을 제공하는가를 기준으로 웹 서버와 WAS를 구별하는 편입니다.
서블릿 컨테이너는 각 서블릿을 싱글톤(인스턴스를 하나만 만들어놓고 재사용)으로 관리하며, 각 서블릿은 멀티 스레딩을 통해 개별 스레드에서 처리됩니다. (서블릿이 싱글톤이라는 점을 이해하면 왜 스프링 서비스 클래스에 지역변수를 두지 말라는지 알 수 있습니다)
서블릿 컨테이너가 제공하는 기능은 딱 여기까지 입니다. 별도의 프레임워크 없이 서블릿을 기반으로 웹 애플리케이션을 개발한다는 것은, 각 요청에 대한 서블릿 클래스를 작성한다는 것입니다. @WebServlet
어노테이션을 사용하거나, XML 설정 파일을 이용하면 각 URL에 대한 서블릿을 서블릿 컨테이너에 등록할 수 있습니다.
아래는 서블릿 클래스 작성 예시입니다. 간단히 /hello 경로로 요청을 받으면 "Hello, Servlet!"을 반환합니다.
@WebServlet("/hello") // "hello" 경로로 요청을 받을 수 있도록 설정
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 모델 데이터 설정
String message = "Hello, MVC Servlet!";
request.setAttribute("message", message);
// 뷰로 포워딩
RequestDispatcher dispatcher = request.getRequestDispatcher("hello.jsp");
dispatcher.forward(request, response);
}
}
서블릿의 요청을 역할에 따라 분리하는 패턴을 MVC 패턴이라고 부릅니다. 지금은 MVC 패턴이 이미 적용된 상태로 이야기하겠습니다. 따라서 각 서블릿은 컨트롤러입니다. 요청을 처리(Handle)하기 때문에 보다 넓은 의미에서 핸들러라고도 합니다. MVC 패턴에 대한 자세한 내용은 이전 포스트를 참고해주세요.
이렇게 컨트롤러를 작성해 요청을 처리하면 반드시 다음과 같은 과정을 따라야합니다. 세세한 동작 방식은 크게 중요하지 않습니다. 다만 매 요청 같은 형식을 띈다는 사실만 봐주세요.
HttpServlet
을 구현하며, service()
를 오버라이딩하여 로직을 정의합니다.service()
는 반드시 HttpServletRequest request
와 HttpServletResponse response
를 받습니다.request
에서 받은 내용을 조회해야 합니다.request
를 사용합니다)과 response
를 뷰(JSP 등)에 넘겨줘야 합니다.4번을 제외한 부분이 항상 중복이 됩니다. 딱 봐도 저 부분만 따로 떼오고 싶은 욕구가 듭니다. 비즈니스 로직 부분을 제외한 부분을 '공통 로직'이라고 부르겠습니다.
이런 형태에서 공통 로직 부분을 떼오면 아래와 같은 형태로 만들 수 있습니다.
위와 같은 형태를 FrontController 패턴이라고 합니다. FrontController 패턴은 다음과 같은 특징을 가지고 있습니다.
이러한 FrontController를 구현한 것이 Spring Web MVC의 DispatcherServlet
입니다.
이전에 살펴본 FrontController를 실제로 구현하려다 보면 몇가지 문제점을 마주하게 됩니다.
이러한 부분을 해결하기 위한 로직과 데이터 및 클래스가 추가됩니다.
하나씩 살펴보겠습니다.
DispatcherServlet
에는 handlerMappings
가 있습니다. 핸들러 종류는 ApplicationContext
로부터 주입받습니다. 그 안에서 요청에 대응하는 핸들러를 찾습니다.DispatcherServlet
에는 handlerAdapters
가 있고 마찬가지로, 스프링에서 주입받습니다. 그 안에서 핸들러에 대응하는 핸들러어댑터를 찾습니다.handle()
을 호출합니다.ModelAndView
를 반환합니다.ViewResolver
를 통해 적절한 View
객체를 반환 받습니다.View
객체를 렌더링하여 최종 응답(HTML)을 반환합니다.복잡해보이지만 이러한 과정은 모두 Spring Web MVC가 추상화하여 제공하는 기능입니다. 개발자는 핸들러 관리와 서블릿 컨테이너의 동작을 신경쓰지 않고 HelloController
즉, 각 요청에 대한 핸들러 로직 작성에 집중할 수 있습니다.
스프링 등 프레임워크나 라이브러리 모두 결국 코드이며 하나의 추상화라는 생각을 하면 받아들이기 편해지는 것 같습니다. 이러한 추상화의 내부 구조를 이해하는 것이 당장 직접적으로 생산성으로 이어지지 않을지 몰라도, 다른 기술이나 문제를 만났을 때, 시스템을 설계할 때 큰 도움이 된다고 생각합니다. 개인적으로 내가 사용하는 기술과 더 친근해지는 것 같아 즐거운 것 같습니다ㅋㅋㅋ.
다음에는 HttpMessageConverter와 ArgumentResolver, ReturnValueHandler 등 Spring Web MVC의 세부적인 처리방식에 대해 정리해보겠습니다.
강의에 있는 익숙한 그림이 아닌걸 보니 다 만드셨나보네요! 구조를 직접 다 그리면서 글을 작성해서 기억에 오래 남을 것 같아요.