Foundations · 원리
Servlet은 무엇이고, Spring은 어떻게 그 위에 올라타나요?
서블릿(Servlet)의 생명주기, 요청-응답 흐름, Filter·Listener, 그리고 내장 톰캣(Tomcat)과 Spring Boot가 맞물리는 방식까지 — 웹 프레임워크의 가장 아래 계층을 인터랙티브 시각화로 해부합니다.
2026년 4월 19일 · 약 15분 읽기
Q. "Servlet이 무엇인지 설명하고, Spring Boot 애플리케이션이 내장 톰캣(Tomcat) 위에서 어떻게 요청을 처리하는지 순서대로 이야기해주세요. 이 과정에서 Filter, Listener, DispatcherServlet의 역할은 각각 무엇인가요?"
예상 꼬리질문
답변 가이드
"서블릿(Servlet)은 자바로 작성된 서버 측 프로그램을 정의하는 표준 스펙으로, jakarta.servlet.Servlet 인터페이스를 구현한 클래스는 HTTP 요청을 받아 응답을 생성할 수 있습니다. 서블릿 컨테이너(Tomcat)는 TCP 소켓과 HTTP 파싱을 전담하고, 파싱한 HttpServletRequest·HttpServletResponse 객체를 Servlet에 넘겨줍니다. Spring MVC의 DispatcherServlet은 이 스펙 위에 올라탄 단 하나의 서블릿일 뿐이며, 그 내부에서 컨트롤러 라우팅·뷰 렌더링이 일어납니다."
"Servlet은 싱글톤(Singleton)으로 생성되어 재사용되므로, init()은 최초 한 번만 호출되고 이후 모든 요청은 서로 다른 스레드에서 service()를 공유합니다. 요청 전·후에 횡단 관심사(인증, 로깅, CORS 등)를 처리하고 싶다면 필터 체인(Filter Chain)에 필터를 등록해 DispatcherServlet 바깥에서 요청을 가로채야 합니다. 필터는 컨테이너 레벨에서 동작하므로 Spring 컨텍스트 바깥의 요청도 처리할 수 있다는 점에서 인터셉터(Interceptor)와 다릅니다."
"Spring Boot는 spring-boot-starter-web 의존성 하나로 내장 톰캣을 구동하고, DispatcherServletAutoConfiguration이 DispatcherServlet을 / 경로에 등록합니다. 실무에서는 레거시 프로토콜 처리가 필요할 때만 ServletRegistrationBean으로 추가 Servlet을 붙이고, 필터는 FilterRegistrationBean과 setOrder()로 실행 순서를 명시합니다. Spring Boot 3.x로 올릴 때는 javax.servlet → jakarta.servlet 패키지 네임스페이스 전환 때문에 임포트 경로와 의존성 버전을 가장 먼저 점검해야 합니다."
"Spring은 어떻게 HTTP 요청을 받아 처리하나요?"라는 질문은 사실 Spring보다 한 층 아래 이야기입니다. Spring이 요청을 받기 전에 소켓을 열고, HTTP를 파싱하고, 스레드를 할당하는 존재가 있기 때문입니다. 그 존재가 바로 서블릿 컨테이너(Tomcat)이고, 그 위에서 동작하는 자바 표준 인터페이스가 서블릿(Servlet)입니다.
이 아티클은 DispatcherServlet 내부가 아닌 그 바깥, 컨테이너와 Servlet 스펙 자체에 집중합니다. 생명주기, 요청-응답 객체의 생성 과정, Filter Chain, 그리고 Spring Boot가 이 모든 것을 어떻게 자동으로 조립하는지를 인터랙티브 시각화로 확인합니다.
1. Servlet 생명주기 — 단 한 번의 init, 수많은 service
꼬리질문: "Servlet의 생명주기 메서드(init, service, destroy)는 각각 언제, 몇 번 호출되나요?"
서블릿은 컨테이너가 관리하는 세 개의 콜백 메서드로 생명주기가 정의됩니다.init(ServletConfig)은 첫 요청이 도착하거나 load-on-startup이 지정된 경우 서버 기동 시점에 단 한 번 호출되어 데이터베이스 커넥션 풀, 설정 로드 같은 초기화를 수행합니다. 이후 모든 요청은 service(req, res)를 호출하고, 내부에서 HTTP 메서드에 따라 doGet, doPost 등으로 분기됩니다. 서버가 종료되거나 애플리케이션이 언디플로이될 때 destroy()가 한 번 호출되어 리소스를 정리합니다.
여기서 가장 중요한 사실은 Servlet 인스턴스는 싱글톤이라는 점입니다. 하나의 인스턴스가 여러 스레드에서 동시에 service()를 실행하므로, 인스턴스 변수에 요청별 상태를 저장하면 다른 스레드의 요청이 그 값을 덮어쓰는 동시성 버그가 발생합니다. 요청 스코프의 데이터는 반드시 지역 변수나 ThreadLocal에 두어야 합니다.
"서버 시작" → "요청 추가" → "서버 종료" 버튼으로 생명주기 각 단계를 직접 트리거하고, 여러 스레드가 하나의 인스턴스를 공유하는 모습을 타임라인에서 확인해보세요.
Servlet 생명주기 타임라인
init 1회 → service N회(여러 스레드 공유) → destroy 1회 흐름을 직접 조작해보세요
init(ServletConfig)
서버 기동 시 또는 첫 요청 도착 시 단 1회
// DB 커넥션 풀 초기화, 설정 로드 등 dataSource = new HikariDataSource(cfg);
단일 Servlet 인스턴스 (싱글톤)
HelloServlet @ 0x1A2B
인스턴스 변수: dataSource, config
// 서버를 시작하면 로그가 표시됩니다
load-on-startup 값이 음수이면 첫 요청이 도착할 때까지 인스턴스 생성이 지연됩니다. 양수 값을 주면 서버 시작 시 지정한 순서대로 init()이 실행되어, 시작 시점의 예열(warm-up) 비용을 런타임에서 분리할 수 있습니다.
2. 요청 객체(HttpServletRequest)는 어떻게 만들어지나
꼬리질문: "HttpServletRequest와 HttpServletResponse는 어떻게 만들어지고 응답 커밋은 무엇인가요?"
클라이언트가 보낸 HTTP 요청은 사실 바이트 스트림에 불과합니다. 이 바이트를 받아 의미 있는 객체로 변환하는 일은 서블릿 컨테이너(Tomcat)가 담당합니다. 톰캣은 소켓에서 바이트를 읽어 요청 라인(Request Line), 헤더(Header), 본문(Body)을 파싱하고, 그 결과를 HttpServletRequest로 래핑해 Servlet에 넘깁니다.
응답도 대칭적으로 동작합니다. HttpServletResponse는 상태 코드·헤더·본문 버퍼를 품고 있다가, 개발자가 getWriter()로 본문을 쓰기 시작하면 내부적으로 버퍼가 채워집니다. 버퍼가 플러시(flush)되어 첫 바이트가 클라이언트로 나가는 순간을 응답 커밋(committed)이라 부르며, 커밋된 이후에는 상태 코드나 헤더를 수정할 수 없습니다.sendRedirect()나 setStatus() 같은 헤더 조작은 반드시 본문을 쓰기 전에 수행해야 합니다.
HTTP 원본 텍스트를 수정한 뒤 "파싱 실행"으로 Request Line / Headers / Body가 객체 필드로 매핑되는 과정을 확인하고, 응답 커밋 이후 헤더 수정을 시도해 IllegalStateException을 체험해보세요.
HTTP 요청 파싱 & 응답 커밋
바이트 → HttpServletRequest 파싱과 응답 버퍼 커밋 시점을 직접 조작해보세요
요청 (Request)
응답 (Response)
response.isCommitted() = false
Status: (unset)
3. Filter Chain — 요청과 응답의 양방향 파이프라인
꼬리질문: "Filter와 Interceptor의 차이는 무엇이며, 필터 체인의 실행 순서는 어떻게 결정되나요?"
필터(Filter)는 Servlet보다 먼저 요청을 받고, Servlet이 응답을 만든 뒤에 다시 그 응답을 받아보는 양방향 미들웨어입니다. 여러 필터는 체인(Chain) 구조로 연결되며, 각 필터의 doFilter(req, res, chain) 메서드에서 chain.doFilter()를 호출하는 시점을 기준으로 앞쪽은 전처리(pre-processing), 뒤쪽은 후처리(post-processing)가 됩니다.
실무에서 가장 많이 실수하는 지점은 필터의 실행 순서입니다. Spring Boot에서 여러 FilterRegistrationBean을 등록했을 때 순서를 명시하지 않으면 빈 이름 사전순으로 결정되어, 인증 필터보다 로깅 필터가 먼저 실행되는 사고가 생길 수 있습니다. 반드시 setOrder() 또는 @Order로 순서를 고정하고, 숫자가 작을수록 먼저 실행된다는 규칙을 기억해야 합니다.
"요청 시작"으로 체인 통과를 재생하고, "인증 실패" 시나리오로 chain.doFilter()를 건너뛸 때 뒷 단계가 차단되는 모습을 비교해보세요.
Filter Chain 양방향 파이프라인
필터 순서를 바꾸거나 인증 실패 시나리오를 실행해 전/후처리 흐름을 확인하세요
Auth Filter
order=1
Logging Filter
order=2
CORS Filter
order=3
Servlet
doGet/doPost
시뮬레이션을 실행하면 요청/응답 흐름이 표시됩니다
필터는 서블릿 컨테이너 레벨에서 동작하므로 Spring 컨텍스트가 로딩되기 전의 요청이나 정적 리소스 요청에도 적용할 수 있습니다. 반면 Spring MVC의 인터셉터(Interceptor)는 DispatcherServlet 내부에서 컨트롤러 앞뒤로만 동작하므로, 적용 범위와 추상화 수준이 다릅니다.
4. Servlet 등록 3가지 방식 — web.xml, @WebServlet, ServletRegistrationBean
꼬리질문: "Spring Boot에서 커스텀 Servlet을 추가할 때 @WebServlet과 ServletRegistrationBean 중 어떤 것을 선호하시나요?"
Servlet을 컨테이너에 등록하는 방법은 역사적으로 세 가지입니다. web.xml은 Servlet 2.5 이전부터 사용된 XML 설정 방식으로, 모든 Servlet·Filter·Listener를 한 파일에서 조망할 수 있다는 장점이 있지만 장황하고 타입 안전하지 않습니다. @WebServlet 어노테이션은 Servlet 3.0에서 도입되어 XML 없이 코드만으로 등록이 가능하지만, 컴포넌트 스캔에 의존하므로 런타임에 등록 결과를 한눈에 확인하기 어렵습니다.
Spring Boot가 권장하는 방식은 ServletRegistrationBean입니다.@Bean으로 서블릿을 등록하면 Spring 컨텍스트가 빈의 생명주기와 의존성 주입을 함께 관리할 수 있고, URL 패턴·loadOnStartup·initParameters 같은 설정을 타입 안전하게 지정할 수 있습니다. 특히 DispatcherServlet 자신도 DispatcherServletRegistrationBean을 통해 등록된다는 점에서, 이 방식은 Spring Boot 자동 설정의 표준 패턴이기도 합니다.
세 탭을 전환하며 동일한 Servlet을 각 방식으로 등록할 때의 코드 차이와 트레이드오프를 비교해보세요.
Servlet 등록 3가지 방식 비교
동일한 Servlet을 등록하는 web.xml · @WebServlet · ServletRegistrationBean의 차이를 비교하세요
import org.springframework.boot.web.servlet
.ServletRegistrationBean;
@Configuration
public class ServletConfig {
@Bean
public ServletRegistrationBean<MyServlet>
myServletBean(DataSource ds) {
MyServlet servlet = new MyServlet(ds); // DI 가능
ServletRegistrationBean<MyServlet> bean =
new ServletRegistrationBean<>(servlet, "/api/*");
bean.setLoadOnStartup(1);
bean.setName("myServlet");
return bean;
}
}권장 맥락: Spring Boot 전반 — 표준 권장 방식
등록 시점
Spring 컨텍스트 초기화 시 빈 생성 → ServletContext 등록
장점
- ✓ 타입 안전 + IDE 자동완성
- ✓ Spring 의존성 주입 활용
- ✓ URL 패턴·initParams 조건부 설정 가능
- ✓ DispatcherServlet 자신도 이 방식으로 등록됨
단점
- ✗ Spring 컨텍스트 의존 (비-Spring 환경 불가)
특징 매트릭스
| 지표 | web.xml | @WebServlet | ServletRegistrationBean |
|---|---|---|---|
| 타입 안전성 | ✗ | ✓ | ✓ |
| Spring 의존성 주입 | ✗ | ✗ | ✓ |
| 런타임 동적 설정 | ✗ | ✗ | ✓ |
| 가시성 (한 곳 조망) | ✓ | ✗ | ✓ |
5. Spring Boot 내장 톰캣 — 자동 조립의 전체 그림
꼬리질문: "Spring Boot가 내장 톰캣과 DispatcherServlet을 자동으로 조립하는 과정은 어떻게 되나요?"
Spring Boot 애플리케이션이 java -jar app.jar로 실행되면 실제로 무슨 일이 벌어지는지 따라가 봅니다.SpringApplication.run()이 호출되면 ServletWebServerApplicationContext가 생성되고, 이 컨텍스트가 ServletWebServerFactory 빈(기본값: TomcatServletWebServerFactory)을 찾아 내장 톰캣을 구동합니다. 톰캣은 지정된 포트(기본 8080)에서 소켓을 열고, ServletContext를 초기화합니다.
이때 Spring Boot의 자동 설정(DispatcherServletAutoConfiguration)이 DispatcherServlet을 생성해 / 경로에 매핑하고, 추가로 등록된 FilterRegistrationBean·ServletRegistrationBean·ServletListenerRegistrationBean들이 차례로 ServletContext에 등록됩니다. 결과적으로 외부 WAS 설치 없이, 하나의 JAR 파일과 한 줄의 main 메서드만으로 완전한 서블릿 애플리케이션이 기동됩니다.
"다음 단계" 버튼을 눌러 6단계 부팅 시퀀스를 따라가고, 자동 설정이 꺼졌을 때 DispatcherServlet이 등록되지 않아 404가 발생하는 시나리오도 관찰해보세요.
Spring Boot 내장 톰캣 부팅 시퀀스
`java -jar` 실행부터 DispatcherServlet 등록까지 6단계를 따라가보세요
부팅 단계
1. main() 실행
SpringApplication
2. ApplicationContext 생성
ServletWebServerApplicationContext
3. ServletWebServerFactory 탐색
TomcatServletWebServerFactory
4. Tomcat start + ServletContext init
Tomcat / ServletContext
5. DispatcherServlet + Filter 등록
DispatcherServletRegistrationBean
6. 포트 8080 수신 대기
Tomcat Connector
콘솔 로그
$ java -jar demo.jar
자동 등록된 빈 목록
JAR 실행 vs 외부 WAS 배포
JAR (내장 톰캣)
6단계 · 단일 JAR 파일
외부 WAS (WAR 배포)
10+단계 · WAS 설치·설정·WAR 복사 필요
spring-boot-starter-web 대신 spring-boot-starter-jetty나 spring-boot-starter-undertow로 의존성을 교체하면 내장 서버가 바뀌지만, Servlet 스펙을 따르는 한 애플리케이션 코드는 그대로 동작합니다. 이것이 Servlet 표준화가 주는 이식성(portability)의 가치입니다.
자주 발생하는 문제
실무와 면접에서 자주 마주치는 Servlet·Filter 관련 문제들입니다. 각 항목을 클릭하여 상황, 원인, 해결 방법을 확인하세요.
핵심 정리
- — Servlet = 스펙, Tomcat = 구현, Spring = 그 위의 프레임워크: DispatcherServlet도 결국 하나의 Servlet일 뿐이다.
- — 생명주기 3메서드: init() 1회 → service() N회(싱글톤 공유) → destroy() 1회. 싱글톤이라는 점을 잊지 말 것.
- — 요청·응답은 컨테이너가 파싱한 결과: 바이트를 HttpServletRequest로, 버퍼를 HttpServletResponse로 변환하는 주체는 Tomcat이다. 응답 커밋 이후 헤더 수정은 불가.
- — Filter는 양방향 미들웨어: chain.doFilter() 앞은 전처리, 뒤는 후처리. 순서는 반드시 setOrder()로 고정.
- — Spring Boot 자동 설정: DispatcherServletAutoConfiguration + 내장 톰캣이 JAR 하나로 완전한 서블릿 애플리케이션을 기동시킨다.
더 알아볼 주제
DispatcherServlet 내부의 HandlerMapping 동작, Servlet 3.1 비동기(async) 요청 처리, 톰캣 커넥터(NIO·NIO2·APR) 비교, Jakarta EE 패키지 마이그레이션 전략, Reactive 스택(WebFlux)이 Servlet을 대체하는 방식
퀴즈로 확인하기
배운 내용을 실제 코드 시나리오에 적용해보세요.
init() 호출 횟수
@WebServlet(urlPatterns = "/api/hello", loadOnStartup = 1)
public class HelloServlet extends HttpServlet {
@Override
public void init(ServletConfig config) { ... }
}
// 하루 동안 100만 건의 요청 처리됨
// init()은 몇 번 호출되었을까요?Filter 실행 순서
authFilter.setOrder(10); loggingFilter.setOrder(1); corsFilter.setOrder(5); // 요청이 Servlet에 도달하기 전, // 세 필터는 어떤 순서로 실행될까요?
응답 커밋 이후 동작
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws IOException {
PrintWriter w = resp.getWriter();
w.println("<h1>Hello</h1>");
w.flush(); // (1)
resp.sendRedirect("/login"); // (2)
}Servlet 싱글톤과 인스턴스 변수
public class OrderServlet extends HttpServlet {
private String userId; // (!)
@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) {
userId = req.getParameter("userId");
// userId로 주문 조회 후 응답 작성
}
}DispatcherServlet의 정체
Spring Boot 애플리케이션에서 DispatcherServlet은 어떻게 정의되나요? - Tomcat과 별개의 프로토콜 처리기일까? - Servlet 스펙을 따르는 하나의 Servlet일까? - Filter와 같은 계층의 미들웨어일까?
추가 학습 자료를 공유합니다.
- Jakarta Servlet Specification 6.0 — Servlet 스펙 원문입니다. 생명주기, 요청·응답 객체, Filter·Listener 계약을 가장 정확하게 확인할 수 있는 1차 자료입니다.
- Spring Boot Reference — Embedded Servlet Container — 내장 톰캣·제티·언더토우 구성, 커스텀 Servlet·Filter·Listener 등록 방법, SSL 설정까지 공식 가이드에서 다루는 실무 핵심 문서입니다.
- Baeldung — Intro to Java Servlets — Servlet API의 기본기를 코드 예제 중심으로 설명한 입문 자료로, Spring을 쓰기 전 Servlet 자체의 동작을 익히는 데 적합합니다.
- Apache Tomcat 10.1 Architecture — 톰캣 내부의 커넥터(Connector), 컨테이너(Engine·Host·Context·Wrapper) 계층 구조를 다룬 문서입니다. "Servlet Container가 실제로 어떻게 생겼는가"를 깊이 있게 이해할 수 있습니다.
의견을 들려주세요
서비스 개선에 큰 도움이 됩니다. 익명으로 자유롭게 남겨주세요.