<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Servlet on kastori</title><link>http://blog.kastori.dev/tags/servlet/</link><description>Recent content in Servlet on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/servlet/index.xml" rel="self" type="application/rss+xml"/><item><title>[Spring 완전 정복 #1] Spring을 제대로 이해하려면 Servlet부터 — HTTP 요청이 Controller에 닿기까지</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-01-servlet-fundamentals/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-01-servlet-fundamentals/</guid><description>&lt;h2 id="spring을-써도-servlet을-알아야-하는-이유"&gt;&lt;a href="#spring%ec%9d%84-%ec%8d%a8%eb%8f%84-servlet%ec%9d%84-%ec%95%8c%ec%95%84%ec%95%bc-%ed%95%98%eb%8a%94-%ec%9d%b4%ec%9c%a0" class="header-anchor"&gt;&lt;/a&gt;Spring을 써도 Servlet을 알아야 하는 이유
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;Filter와 Interceptor의 차이가 뭔가요?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;면접에서 자주 나오는 질문이다. 많은 개발자들이 &amp;ldquo;Filter는 Servlet 앞, Interceptor는 Controller 앞&amp;quot;이라고 외워서 답한다. 맞는 말이지만, &lt;em&gt;왜&lt;/em&gt; 그런지를 이해하고 있는 사람은 적다.&lt;/p&gt;
&lt;p&gt;그 이유를 이해하려면 HTTP 요청이 Spring Controller에 닿기까지 거치는 전체 레이어를 알아야 한다. 그리고 그 출발점은 &lt;strong&gt;Servlet&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="cgi에서-servlet으로--왜-바뀌었나"&gt;&lt;a href="#cgi%ec%97%90%ec%84%9c-servlet%ec%9c%bc%eb%a1%9c--%ec%99%9c-%eb%b0%94%eb%80%8c%ec%97%88%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;CGI에서 Servlet으로 — 왜 바뀌었나
&lt;/h2&gt;&lt;p&gt;초창기 웹 서버는 정적 파일만 제공했다. 동적 응답(사용자별 맞춤 페이지, DB 조회 결과 등)이 필요해지자 **CGI(Common Gateway Interface)**가 등장했다.&lt;/p&gt;
&lt;p&gt;CGI의 방식은 단순하다: HTTP 요청이 들어올 때마다 새 프로세스를 생성해서 처리하고, 끝나면 종료한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;요청 1 → 프로세스 생성 → 처리 → 프로세스 종료
요청 2 → 프로세스 생성 → 처리 → 프로세스 종료
요청 N → ...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;문제는 프로세스 생성 비용이다. 동시 접속자가 늘어나면 서버가 버티지 못한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java Servlet은 이 문제를 스레드로 해결했다.&lt;/strong&gt; Servlet 인스턴스는 JVM에 딱 1개만 상주하고, 요청마다 스레드를 하나씩 할당해 처리한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Servlet 인스턴스 (1개, JVM에 상주)
 ├─ 요청 1 → 스레드 1
 ├─ 요청 2 → 스레드 2
 └─ 요청 N → 스레드 N
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;인스턴스를 매번 새로 만들지 않으니 메모리와 시간 비용이 크게 줄었다. 이 설계가 오늘날 Spring 애플리케이션의 근간이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="servlet-생명주기--인스턴스는-한-번만-만들어진다"&gt;&lt;a href="#servlet-%ec%83%9d%eb%aa%85%ec%a3%bc%ea%b8%b0--%ec%9d%b8%ec%8a%a4%ed%84%b4%ec%8a%a4%eb%8a%94-%ed%95%9c-%eb%b2%88%eb%a7%8c-%eb%a7%8c%eb%93%a4%ec%96%b4%ec%a7%84%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;Servlet 생명주기 — 인스턴스는 한 번만 만들어진다
&lt;/h2&gt;&lt;p&gt;Servlet 인스턴스는 **Servlet Container(Tomcat)**가 관리한다. 생명주기는 세 단계다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;최초 요청 또는 서버 시작
 → init() : 인스턴스 생성 + 초기화 (딱 1회)
 → service() : 요청마다 호출 (doGet / doPost 등으로 분기)
 → destroy() : 컨테이너 종료 시 (딱 1회)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;코드로 보면 이렇다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyServlet&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; HttpServlet {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// DB 커넥션 풀 초기화 등 1회성 작업&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;doGet&lt;/span&gt;(HttpServletRequest req, HttpServletResponse res)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; IOException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.&lt;span style="color:#a6e22e"&gt;getWriter&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;write&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Hello&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;destroy&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 리소스 해제&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;직접 이런 코드를 쓸 일은 거의 없다. 하지만 Spring의 &lt;code&gt;DispatcherServlet&lt;/code&gt;이 내부적으로 이 구조를 그대로 따른다는 점이 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="tomcat의-역할--spring-boot가-jar-하나로-뜨는-이유"&gt;&lt;a href="#tomcat%ec%9d%98-%ec%97%ad%ed%95%a0--spring-boot%ea%b0%80-jar-%ed%95%98%eb%82%98%eb%a1%9c-%eb%9c%a8%eb%8a%94-%ec%9d%b4%ec%9c%a0" class="header-anchor"&gt;&lt;/a&gt;Tomcat의 역할 — Spring Boot가 jar 하나로 뜨는 이유
&lt;/h2&gt;&lt;p&gt;Servlet Container인 Tomcat은 단순히 Servlet을 실행하는 것 이상의 역할을 한다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;역할&lt;/th&gt;
 &lt;th&gt;설명&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Servlet 인스턴스 관리&lt;/td&gt;
 &lt;td&gt;생성, 초기화, 소멸&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;스레드 풀 관리&lt;/td&gt;
 &lt;td&gt;요청마다 스레드 할당&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;HTTP 파싱&lt;/td&gt;
 &lt;td&gt;원시 HTTP 바이트 스트림 → &lt;code&gt;HttpServletRequest&lt;/code&gt; 객체 변환&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;URL 매핑&lt;/td&gt;
 &lt;td&gt;어떤 URL을 어떤 Servlet이 처리할지 결정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Filter Chain 실행&lt;/td&gt;
 &lt;td&gt;Servlet 앞뒤로 Filter 실행&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Spring Boot가 별도의 WAS 없이 &lt;code&gt;java -jar&lt;/code&gt; 한 줄로 서버를 시작할 수 있는 이유는 **Tomcat을 내장(embedded)**하기 때문이다. 애플리케이션 안에 Tomcat이 들어있으니 따로 설치할 필요가 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="filter--spring보다-바깥-레이어"&gt;&lt;a href="#filter--spring%eb%b3%b4%eb%8b%a4-%eb%b0%94%ea%b9%a5-%eb%a0%88%ec%9d%b4%ec%96%b4" class="header-anchor"&gt;&lt;/a&gt;Filter — Spring보다 바깥 레이어
&lt;/h2&gt;&lt;p&gt;Filter는 Servlet Container 레벨에서 동작한다. Spring Context가 시작되기 전, 즉 DispatcherServlet보다 앞에 위치한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HTTP 요청 → [Filter1 → Filter2 → Filter3] → Servlet(DispatcherServlet)
HTTP 응답 ← [Filter1 ← Filter2 ← Filter3] ← Servlet
&lt;/code&gt;&lt;/pre&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LoggingFilter&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; Filter {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;doFilter&lt;/span&gt;(ServletRequest request, ServletResponse response,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FilterChain chain) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; IOException, ServletException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; System.&lt;span style="color:#a6e22e"&gt;out&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;println&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;요청 진입: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; ((HttpServletRequest) request).&lt;span style="color:#a6e22e"&gt;getRequestURI&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chain.&lt;span style="color:#a6e22e"&gt;doFilter&lt;/span&gt;(request, response); &lt;span style="color:#75715e"&gt;// 다음 Filter 또는 Servlet으로 넘김&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; System.&lt;span style="color:#a6e22e"&gt;out&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;println&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;응답 완료&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Filter가 적합한 작업:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인코딩 설정&lt;/strong&gt;: 모든 요청에 &lt;code&gt;UTF-8&lt;/code&gt; 적용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS 처리&lt;/strong&gt;: 응답 헤더 추가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인증 토큰 1차 검사&lt;/strong&gt;: Authorization 헤더 존재 여부 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;요청/응답 로깅&lt;/strong&gt;: URL, 처리 시간 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한 가지 중요한 제약이 있다. Filter는 Spring Context 밖에 있기 때문에 &lt;strong&gt;Spring의 예외 처리(&lt;code&gt;@ControllerAdvice&lt;/code&gt;)가 적용되지 않는다.&lt;/strong&gt; Filter에서 예외가 발생하면 직접 처리해야 한다.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Spring Security의 필터체인도 Servlet Filter다. 이 때문에 Security 설정이 Spring MVC(Controller, Interceptor 등)보다 먼저 동작한다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="dispatcherservlet--spring-mvc의-시작점"&gt;&lt;a href="#dispatcherservlet--spring-mvc%ec%9d%98-%ec%8b%9c%ec%9e%91%ec%a0%90" class="header-anchor"&gt;&lt;/a&gt;DispatcherServlet — Spring MVC의 시작점
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;DispatcherServlet&lt;/code&gt;은 Spring MVC의 핵심이지만, 결국 &lt;code&gt;HttpServlet&lt;/code&gt;을 상속한 &lt;strong&gt;Servlet&lt;/strong&gt;이다. Tomcat이 관리하는 수많은 Servlet 인스턴스 중 하나일 뿐이다.&lt;/p&gt;
&lt;p&gt;Spring Boot는 &lt;code&gt;DispatcherServlet&lt;/code&gt;을 자동 등록하고, 모든 URL(&lt;code&gt;/&lt;/code&gt;)을 이 Servlet이 받도록 설정한다. 이후 요청을 어떤 Controller로 보낼지는 &lt;code&gt;DispatcherServlet&lt;/code&gt; 내부의 &lt;code&gt;HandlerMapping&lt;/code&gt;이 결정한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="전체-요청-흐름--한눈에-보기"&gt;&lt;a href="#%ec%a0%84%ec%b2%b4-%ec%9a%94%ec%b2%ad-%ed%9d%90%eb%a6%84--%ed%95%9c%eb%88%88%ec%97%90-%eb%b3%b4%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;전체 요청 흐름 — 한눈에 보기
&lt;/h2&gt;&lt;p&gt;HTTP 요청이 Controller에 닿기까지 거치는 전체 레이어다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HTTP 요청
 → Tomcat (HTTP 파싱, 스레드 할당)
 → Filter Chain (Servlet Container 레벨)
 → DispatcherServlet (Spring MVC 진입)
 → Interceptor (preHandle)
 → AOP (Before Advice)
 → Controller 메서드 실행
 → AOP (After Advice)
 → Interceptor (postHandle)
 → DispatcherServlet (ViewResolver 등)
 → Filter Chain (역순)
 → HTTP 응답
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 흐름이 Filter / Interceptor / AOP의 차이를 결정한다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;구분&lt;/th&gt;
 &lt;th&gt;위치&lt;/th&gt;
 &lt;th&gt;Spring Bean 접근&lt;/th&gt;
 &lt;th&gt;Spring 예외 처리&lt;/th&gt;
 &lt;th&gt;주요 용도&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Filter&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Servlet Container&lt;/td&gt;
 &lt;td&gt;제한적&lt;/td&gt;
 &lt;td&gt;X&lt;/td&gt;
 &lt;td&gt;인코딩, CORS, 인증 토큰&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Interceptor&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Spring MVC&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;로그인 체크, 권한 검사&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;AOP&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Spring Bean&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;트랜잭션, 로깅, 성능 측정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;Servlet은 오래된 기술이지만, Spring MVC의 모든 레이어(Filter, DispatcherServlet, Interceptor, AOP)가 이 위에 쌓여있다. 이 구조를 이해하면 &amp;ldquo;Filter와 Interceptor 중 무엇을 써야 하나?&amp;ldquo;라는 질문에 외운 답이 아닌 구조적 이해로 답할 수 있다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Core — IoC 컨테이너와 의존성 주입이 어떻게 동작하는지 정리한다.&lt;/p&gt;</description></item></channel></rss>