DispatcherServlet도 결국 Servlet인데, 어디서 service() 메서드가 호출되고 있는걸까?
이 궁금증을 해결하기 위해 직접 코드를 파헤쳐보고 해당 여정을 기록했다. 코드를 따라가는 단순한 과정이지만, 비슷한 궁금증을 가진 사람들이 있다면 도움이 되기를 바라며 공유한다 😎
🐈⬛ Tomcat 구현하기와 Servlet
Tomcat 구현하기 미션에서 배운 가장 중요한 개념 중 하나는 Servlet의 본질이다. Servlet은 웹 요청을 처리하기 위한 Java 표준 인터페이스이고, 그 핵심은 service() 메서드다.
public interface Servlet {
void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
}
Tomcat 구현하기 미션에서는 이 Servlet 구조를 모방해서 Controller라는 이름의 인터페이스를 만들었다. 미션에서 구현한 Controller는 실제 Servlet과 비슷한 역할을 하고, AbstractController는 HttpServlet과 비슷한 역할을 하도록 설계했다.
Tomcat은 이 Servlet들을 관리하고 실행하는 컨테이너 역할을 한다. 그리고 Spring MVC의 DispatcherServlet도 결국은 이 Servlet 인터페이스를 구현한 클래스다.
그렇다면 모든 HTTP 요청이 결국 DispatcherServlet의 service() 메서드로 도달할 텐데, 실제로는 어떤 경로를 거쳐서 도달하는 걸까?
🌱 @MVC 구현하기와 DispatcherServlet
@MVC 구현하기 미션을 진행하면서, 이전 Tomcat 구현하기에서 배운 Servlet 지식을 바탕으로 Spring MVC의 DispatcherServlet을 더 깊이 이해할 수 있었다.
지금까지 여러 유튜브와 블로그 글을 통해서 다음과 같은 내용을 자주 접했다.
- 스프링부트는 톰캣을 내장하고 있다.
- 스프링 MVC는 DispatcherServlet이 모든 요청을 처리하는 프론트 컨트롤러 패턴이다.
정말 많이 봤지만 제대로 이해했다고 느낀 적은 없었는데, 직접 Servlet을 구현해본 후에야 비로소 이 말들의 의미를 깨달을 수 있었다.
Tomcat은 Servlet 컨테이너이고, DispatcherServlet은 Servlet의 구현체다. 따라서 Tomcat이 HTTP 요청을 받으면 DispatcherServlet의 service() 메서드를 호출하게 되고, 여기서부터 Spring MVC의 요청 처리 과정이 시작되는 것이다.
📍 DispatcherServlet 의 호출 과정
Spring MVC에서는 일반적으로 하나의 서블릿인 DispatcherServlet이 모든 요청을 담당하도록 Tomcat에 등록된다. 그렇다면 모든 HTTP 요청이 DispatcherServlet의 service() 메서드를 통해 처리될 것이다. 그래서 코드를 살펴보기 위해 DispatcherServlet의 service() 메서드를 찾아봤는데, 아무리 찾아도 보이지 않았다.
대신 이름이 비슷해 보이는 doService() 메서드를 발견했다. 여기서부터 추적을 시작해보기로 했다.
1️⃣ DispatcherServlet의 doService() 메서드
DispatcherServlet의 소스 코드를 확인해보니, doService() 메서드 위에 @Override 어노테이션이 붙어있다.
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 실제 요청 처리 로직...
}
이는 부모 클래스에서 이 메서드가 먼저 정의되어 있다는 의미다. DispatcherServlet이 FrameworkServlet을 상속하고 있으니, 다음은 FrameworkServlet에서 doService()가 어떻게 정의되어 있는지 살펴보자.
2️⃣ FrameworkServlet의 doService() 메서드
FrameworkServlet에서 doService() 메서드를 찾아보니, protected abstract로 선언되어 있다.
/**
* Subclasses must implement this method to do the work of request handling,
* receiving a centralized callback for GET, POST, PUT and DELETE.
*/
protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
throws Exception;
주석을 보면 "하위 클래스에서 반드시 구현해야 하는 메서드"라고 명시되어 있다. 즉, FrameworkServlet은 doService()의 구현을 DispatcherServlet에게 위임한 것이다. 그럼 이 메서드는 누가 호출하고 있을까?
3️⃣ FrameworkServlet의 processRequest() 메서드
doService() 메서드는 같은 FrameworkServlet 클래스의 processRequest() 메서드에서 호출되고 있다.
processReqeuest()는 모든 HTTP 메서드 처리에서 호출되고 있다. FrameworkServlet을 살펴보면 다음과 같은 패턴을 발견할 수 있다.
public abstract class FrameworkServlet extends HttpServlet {
protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
throws Exception;
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 사전 처리
try {
doService(request, response); // 여기서 DispatcherServlet의 doService 호출!
}
// 사후 처리
}
// 모든 HTTP 메서드가 processRequest를 호출
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response) {
processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response) {
processRequest(request, response);
}
// doPut, doDelete, doOptions, doTrace도 동일한 패턴
}
FrameworkServlet은 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)에 대해 동일한 처리 방식을 사용한다. 각 doXXX() 메서드는 모두 processRequest()를 호출하고, processRequest()는 다시 doService()를 호출하는 구조다.
여기서 중요한 발견은 FrameworkServlet이 HttpServlet을 상속하고 있다는 점이다. 그렇다면 이 doGet(), doPost() 메서드들은 누가 호출하는 걸까? 답을 찾기 위해 HttpServlet로 올라가보자.
4️⃣ HttpServlet의 service() 메서드
드디어 Jakarta Servlet 표준 영역에 도달했다. HttpServlet은 Servlet 표준의 핵심 클래스 중 하나로, HTTP 프로토콜에 특화된 서블릿 기능을 제공한다.
HttpServlet의 핵심 구조는 다음과 같다:
public abstract class HttpServlet extends GenericServlet {
// 1. Servlet 인터페이스의 service 메서드 구현
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
// ServletRequest를 HttpServletRequest로 캐스팅
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// HTTP 전용 service 메서드 호출
service(request, response);
}
// 2. HTTP 메서드별로 분기하는 service 메서드
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if (method.equals("GET")) {
doGet(req, resp);
} else if (method.equals("POST")) {
doPost(req, resp);
} else if (method.equals("PUT")) {
doPut(req, resp);
} // 다른 HTTP 메서드들도 동일한 패턴
}
// 3. 하위 클래스에서 구현할 메서드들 (기본적으로는 에러 응답)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// "Method Not Allowed" 에러 응답
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
// "Method Not Allowed" 에러 응답
}
}
마침내 우리가 찾던 Servlet 인터페이스의 service() 메서드를 발견했다!
HttpServlet의 동작 방식은 다음과 같다:
- Servlet 컨테이너(Tomcat)가 service(ServletRequest, ServletResponse) 호출
- HttpServlet이 이를 service(HttpServletRequest, HttpServletResponse)로 위임
- HTTP 메서드를 확인해서 적절한 doXXX() 메서드 호출
- 하위 클래스(FrameworkServlet)에서 오버라이드한 doXXX() 메서드 실행
이제 전체 호출 체인이 명확해졌다. Tomcat에서 시작된 요청이 어떻게 DispatcherServlet의 doService()까지 도달하는지 완전히 파악했다!
정리
DispatcherServlet이 정말 Servlet의 일종이라면, service 호출을 통해 실행될텐데 어디서부터 어떻게 호출되는지에 대한 궁금증으로부터 시작해서 끝까지 따라 올라가봤다.
DispatcherServlet의 doService()부터 거꾸로 올라가며 추적한 결과, 다음과 같은 호출 관계를 발견할 수 있었다:
Servlet::service()
→ (Override) GenericServlet::service()
→ (Override) HttpServlet::service()
→ (호출) HttpServlet::service(HttpServletRequest, HttpServletResponse)
→ (Override) FrameworkServlet::service()
→ (호출) FrameworkServlet::processRequest()
→ (호출) FrameworkServlet::doService()
→ (Override) DispatcherServlet::doService()
이런 구조를 통해 거대한 구조 속에서 추상화를 어떻게 활용할 수 있는지 눈으로 확인할 수 있었다.
또한 프레임워크들을 오가며 각 계층의 책임을 대략적으로 이해하게 됐다.
그리고 무엇보다 모르는 것을 직접 찾아보는 재미를 다시 한 번 느꼈다. 단순히 "DispatcherServlet이 요청을 처리한다"는 지식에서 "정확히 어떤 경로로 처리되는가"까지 파악하니까 추상적인 개념들이 손에 잡히는 구체적인 코드로 다가왔다.
이 글을 읽고 흥미가 생겼다면 DispatcherServlet의 doDispatch 메서드를 한번 파헤쳐보는 것을 추천한다. @MVC 구현하기 미션의 코드와 거의 흡사한 구조를 가지고 있어서, 비교하면서 보는 재미가 쏠쏠하다.
'개발 > Java & Spring' 카테고리의 다른 글
| [@MVC 구현하기] Spring MVC의 HandlerMapping 등록 원리 파헤치기 (4) | 2025.09.22 |
|---|---|
| Tomcat 구현하기 삽질기 (feat.acceptCount) (2) | 2025.09.15 |
| Fixture와 Builder 패턴으로 테스트 코드 가독성 높이기 (4) | 2025.07.25 |