Servlet & Filter 완벽 가이드: 요청 흐름부터 Filter vs Interceptor, 실무 구현까지

Servlet & Filter 완벽 가이드: 요청 흐름부터 Filter vs Interceptor, 실무 구현까지

면접에서 “Spring MVC의 요청 흐름을 설명해주세요”라는 질문이 나왔을 때, DispatcherServlet만 말하면 절반만 답한 것이다. 요청이 DispatcherServlet에 도달하기 전에 Servlet Container와 Filter Chain을 거치고, DispatcherServlet 내부에서 Interceptor를 거친다. 이 글은 Servlet의 본질부터 Filter와 Interceptor의 차이, 실무 구현까지 — 웹 요청의 전체 흐름을 다룬다.


1. Servlet이란?

1.1 정의

Servlet은 Java로 HTTP 요청을 처리하는 서버 측 프로그램이다. Java EE(현 Jakarta EE) 스펙의 핵심으로, 웹 서버와 Java 애플리케이션을 연결하는 다리 역할을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────────────────┐
│                    웹 요청 처리의 진화                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1세대: CGI (Common Gateway Interface)                              │
│    요청마다 새 프로세스 생성 → 매우 느림, 리소스 낭비                │
│                                                                     │
│  2세대: Servlet                                                     │
│    요청마다 새 스레드 생성 → 프로세스보다 훨씬 가벼움               │
│    Servlet 객체는 싱글톤으로 재사용                                  │
│                                                                     │
│  3세대: Spring MVC                                                  │
│    DispatcherServlet 하나가 모든 요청을 받고 위임                   │
│    개발자는 Servlet을 직접 작성하지 않음                             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

1.2 Servlet Container (= Web Container)

Servlet은 혼자 실행될 수 없다. Servlet Container(Tomcat, Jetty, Undertow)가 Servlet의 생명주기를 관리하고, HTTP 요청을 Servlet에 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────────────────────────┐
│                    Servlet Container의 역할                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Client ──── HTTP Request ────→ Servlet Container (Tomcat)         │
│                                        │                            │
│                                  ┌─────┴─────┐                     │
│                                  │           │                      │
│                              ① 네트워크     ② Servlet               │
│                                통신 처리     생명주기 관리           │
│                                  │           │                      │
│                              ③ 스레드 풀   ④ HTTP 요청/응답        │
│                                관리          객체 생성               │
│                                  │           │                      │
│                              ⑤ 보안·인증   ⑥ JSP 처리             │
│                                              │                      │
│  Servlet Container가 하는 일:                                       │
│  ● TCP/IP 연결 수립, HTTP 파싱                                     │
│  ● HttpServletRequest / HttpServletResponse 객체 생성              │
│  ● URL에 맞는 Servlet 매핑                                         │
│  ● 스레드 풀에서 스레드 할당                                        │
│  ● Servlet의 service() 메서드 호출                                 │
│  ● 응답을 HTTP로 변환하여 클라이언트에 전송                         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Spring Boot는 내장 Tomcat을 사용하므로 별도 설치가 필요 없다. java -jar myapp.jar만 실행하면 Tomcat이 함께 시작된다.


2. Servlet 생명주기

2.1 생명주기 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────────────────┐
│                    Servlet 생명주기                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Servlet Container 시작                                            │
│         │                                                           │
│         ▼                                                           │
│   ① 클래스 로딩 + 인스턴스 생성 (한 번만)                           │
│         │                                                           │
│         ▼                                                           │
│   ② init() 호출 (한 번만)                                          │
│      └── 초기화 작업 (DB 연결, 설정 로딩 등)                        │
│         │                                                           │
│         ▼                                                           │
│   ┌─────────────────────────────┐                                  │
│   │  ③ service() 호출 (매 요청) │ ◀─── 클라이언트 요청마다 반복    │
│   │     │                       │                                   │
│   │     ├── GET  → doGet()     │      새 스레드에서 실행           │
│   │     ├── POST → doPost()    │      Servlet 객체는 공유 (싱글톤)│
│   │     ├── PUT  → doPut()     │                                   │
│   │     └── DELETE → doDelete()│                                   │
│   └─────────────────────────────┘                                  │
│         │                                                           │
│   (Container 종료 시)                                               │
│         ▼                                                           │
│   ④ destroy() 호출 (한 번만)                                       │
│      └── 리소스 정리 (DB 연결 해제 등)                              │
│                                                                     │
│   핵심: Servlet 인스턴스는 싱글톤!                                   │
│   → 멤버 변수에 상태를 저장하면 Thread-Safety 문제 발생             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

2.2 코드로 보는 생명주기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyServlet extends HttpServlet {

    // ② 초기화 (Container 시작 시 한 번)
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        System.out.println("Servlet 초기화");
    }

    // ③ 요청 처리 (매 요청마다, 각각 다른 스레드에서)
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html; charset=UTF-8");
        resp.getWriter().write("<h1>Hello Servlet</h1>");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String name = req.getParameter("name");
        resp.getWriter().write("이름: " + name);
    }

    // ④ 소멸 (Container 종료 시 한 번)
    @Override
    public void destroy() {
        System.out.println("Servlet 소멸 - 리소스 정리");
    }
}

2.3 Servlet의 Thread-Safety 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 위험한 코드: 싱글톤 Servlet에 멤버 변수 사용
public class DangerousServlet extends HttpServlet {
    private int count = 0; // 모든 스레드가 공유!

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        count++; // Race Condition 발생!
        resp.getWriter().write("Count: " + count);
    }
}

// ✅ 안전한 코드: 지역 변수 또는 ThreadLocal 사용
public class SafeServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        int localCount = 0; // 스레드마다 독립적인 지역 변수
        localCount++;
        resp.getWriter().write("Count: " + localCount);
    }
}

Spring의 Controller도 싱글톤이므로 동일한 원칙이 적용된다. Controller에 상태(멤버 변수)를 저장하면 안 된다.


3. HttpServletRequest & HttpServletResponse

3.1 HttpServletRequest (요청 정보)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {

    // 요청 라인 정보
    req.getMethod();          // "GET", "POST" 등
    req.getRequestURI();      // "/api/users"
    req.getRequestURL();      // "http://localhost:8080/api/users"
    req.getQueryString();     // "page=1&size=10"
    req.getProtocol();        // "HTTP/1.1"

    // 헤더 정보
    req.getHeader("Authorization");  // "Bearer eyJhbG..."
    req.getHeader("Content-Type");   // "application/json"
    req.getContentType();            // "application/json"

    // 파라미터 (쿼리 스트링 + form 데이터)
    req.getParameter("name");            // 단일 값
    req.getParameterValues("category");  // 복수 값 (배열)
    req.getParameterMap();               // 전체 파라미터 Map

    // Body (JSON 등)
    req.getInputStream();    // 바이너리 데이터
    req.getReader();         // 텍스트 데이터 (BufferedReader)

    // 클라이언트 정보
    req.getRemoteAddr();     // 클라이언트 IP
    req.getRemotePort();     // 클라이언트 포트

    // 속성 (Filter → Servlet 간 데이터 전달)
    req.setAttribute("userId", 123L);
    req.getAttribute("userId");

    // 세션
    req.getSession();               // 세션 가져오기 (없으면 생성)
    req.getSession(false);          // 세션 가져오기 (없으면 null)
}

3.2 HttpServletResponse (응답 설정)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {

    // 상태 코드
    resp.setStatus(HttpServletResponse.SC_OK);           // 200
    resp.setStatus(HttpServletResponse.SC_CREATED);      // 201
    resp.sendError(HttpServletResponse.SC_NOT_FOUND);    // 404

    // 헤더
    resp.setHeader("Cache-Control", "no-cache");
    resp.addHeader("Set-Cookie", "token=abc123");
    resp.setContentType("application/json; charset=UTF-8");

    // 응답 Body
    PrintWriter writer = resp.getWriter();
    writer.write("{\"name\": \"홍길동\"}");

    // 리다이렉트
    resp.sendRedirect("/login");
}

3.3 Spring에서의 관계

Spring MVC에서는 HttpServletRequest/HttpServletResponse직접 다루지 않는 것이 원칙이다. Spring이 이를 추상화해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Servlet 방식: 직접 request/response 조작
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String name = req.getParameter("name");
    resp.setContentType("application/json");
    resp.getWriter().write("{\"name\": \"" + name + "\"}");
}

// Spring 방식: 어노테이션으로 추상화
@GetMapping("/users")
public UserResponse getUser(@RequestParam String name) {
    return new UserResponse(name);
    // 파라미터 바인딩, JSON 변환, Content-Type 설정 모두 자동
}

하지만 Filter나 Interceptor에서는 HttpServletRequest직접 사용해야 한다.


4. DispatcherServlet

4.1 프론트 컨트롤러 패턴

Spring MVC의 핵심은 DispatcherServlet — 모든 HTTP 요청을 받아서 적절한 Controller에 위임하는 “대문” 역할의 Servlet이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────────────┐
│                    프론트 컨트롤러 패턴                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Before (Servlet마다 URL 매핑):                                     │
│                                                                     │
│    /users  → UserServlet                                           │
│    /orders → OrderServlet                                          │
│    /items  → ItemServlet                                           │
│    → 공통 로직(인코딩, 인증, 로깅) 중복                             │
│                                                                     │
│  After (DispatcherServlet 하나로 통합):                             │
│                                                                     │
│    /users  ─┐                                                      │
│    /orders ─┼──→ DispatcherServlet ──→ 적절한 Controller로 위임    │
│    /items  ─┘      (Front Controller)                              │
│    → 공통 로직을 한 곳에서 처리                                     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

4.2 DispatcherServlet 동작 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────────┐
│                DispatcherServlet 내부 처리 흐름                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  HTTP 요청                                                          │
│       │                                                             │
│       ▼                                                             │
│  DispatcherServlet.doDispatch()                                     │
│       │                                                             │
│  ① HandlerMapping ─── URL에 맞는 Handler(Controller) 찾기          │
│       │                @GetMapping("/api/users") 매핑 확인          │
│       ▼                                                             │
│  ② HandlerAdapter ─── Handler 실행 (어댑터 패턴)                   │
│       │                파라미터 바인딩, 메서드 호출                  │
│       ▼                                                             │
│  ③ Controller ──────── 비즈니스 로직 실행                           │
│       │                Service → Repository                        │
│       ▼                                                             │
│  ④ ViewResolver ────── 뷰 이름 → 실제 뷰 변환                     │
│       │                (REST API는 HttpMessageConverter로 JSON 변환)│
│       ▼                                                             │
│  HTTP 응답                                                          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

4.3 DispatcherServlet도 결국 Servlet이다

1
2
3
4
5
// DispatcherServlet의 상속 구조
HttpServlet              // Java EE 표준 Servlet
  └── HttpServletBean    // Spring: Servlet 설정을 Bean으로
      └── FrameworkServlet     // Spring: ApplicationContext 연동
          └── DispatcherServlet // Spring MVC: 요청 분배

Spring Boot가 시작되면 자동으로 DispatcherServlet을 생성하고 / 경로에 매핑한다. 모든 요청이 이 Servlet을 거치게 된다.


5. Filter

5.1 Filter란?

Filter는 Servlet Container 레벨에서 동작하며, 요청이 Servlet(DispatcherServlet)에 도달하기 과 응답이 클라이언트로 돌아가기 에 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────────┐
│                    Filter의 위치                                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Client                                                             │
│    │                                                                │
│    ▼                                                                │
│  ┌──────────────── Servlet Container (Tomcat) ──────────────────┐  │
│  │                                                               │  │
│  │  Filter 1 → Filter 2 → Filter 3                              │  │
│  │    │                        │                                 │  │
│  │    │    ┌── Spring MVC ─────┴──────────────────────────┐     │  │
│  │    │    │                                               │     │  │
│  │    │    │  DispatcherServlet                            │     │  │
│  │    │    │       │                                       │     │  │
│  │    │    │  Interceptor 1 → Interceptor 2               │     │  │
│  │    │    │       │                                       │     │  │
│  │    │    │  Controller → Service → Repository           │     │  │
│  │    │    │                                               │     │  │
│  │    │    └───────────────────────────────────────────────┘     │  │
│  │    │                                                          │  │
│  └────┴──────────────────────────────────────────────────────────┘  │
│                                                                     │
│  Filter: Servlet Container 레벨 (Spring 밖)                        │
│  Interceptor: Spring MVC 레벨 (Spring 안)                          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

5.2 Filter 인터페이스

1
2
3
4
5
6
7
8
9
10
11
public interface Filter {
    // Container 시작 시 한 번 호출
    default void init(FilterConfig filterConfig) throws ServletException {}

    // 매 요청마다 호출
    void doFilter(ServletRequest request, ServletResponse response,
                  FilterChain chain) throws IOException, ServletException;

    // Container 종료 시 한 번 호출
    default void destroy() {}
}

5.3 Filter Chain 동작 원리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌─────────────────────────────────────────────────────────────────────┐
│                    Filter Chain 동작                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  요청(Request) →                                                    │
│                                                                     │
│  ┌─────────────────┐                                               │
│  │    Filter A      │                                               │
│  │  ┌────────────┐  │                                               │
│  │  │ 전처리 코드 │  │  ← 요청이 Servlet에 도달하기 전              │
│  │  └────────────┘  │                                               │
│  │  chain.doFilter()│  ← 다음 Filter 또는 Servlet으로 넘김         │
│  │  ┌────────────┐  │                                               │
│  │  │ 후처리 코드 │  │  ← Servlet 처리 후 응답이 돌아올 때          │
│  │  └────────────┘  │                                               │
│  └─────────────────┘                                               │
│          │                                                          │
│          ▼                                                          │
│  ┌─────────────────┐                                               │
│  │    Filter B      │                                               │
│  │  전처리          │                                               │
│  │  chain.doFilter()│                                               │
│  │  후처리          │                                               │
│  └─────────────────┘                                               │
│          │                                                          │
│          ▼                                                          │
│  ┌─────────────────┐                                               │
│  │    Servlet       │ (DispatcherServlet)                           │
│  │  요청 처리       │                                               │
│  └─────────────────┘                                               │
│                                                                     │
│  핵심: chain.doFilter()를 호출하지 않으면 요청이 여기서 멈춤!       │
│  → 인증 실패 시 chain.doFilter()를 호출하지 않고 에러 응답          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

5.4 Filter 구현 예시

로깅 Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@Component
@Order(1) // Filter 순서 지정 (낮을수록 먼저 실행)
public class LoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        long startTime = System.currentTimeMillis();

        log.info("[REQUEST] {} {} from {}", req.getMethod(), req.getRequestURI(),
                req.getRemoteAddr());

        chain.doFilter(request, response); // 다음 Filter 또는 Servlet으로

        long duration = System.currentTimeMillis() - startTime;
        HttpServletResponse resp = (HttpServletResponse) response;
        log.info("[RESPONSE] {} {} - {}ms (status: {})",
                req.getMethod(), req.getRequestURI(), duration, resp.getStatus());
    }
}

인코딩 Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Order(0) // 가장 먼저 실행
public class EncodingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        chain.doFilter(request, response);
    }
}

인증 Filter (chain.doFilter 호출하지 않는 경우)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j
@Component
@Order(2)
public class AuthenticationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        String token = req.getHeader("Authorization");

        // 인증이 필요 없는 경로는 통과
        String uri = req.getRequestURI();
        if (uri.startsWith("/api/public") || uri.equals("/api/login")) {
            chain.doFilter(request, response);
            return;
        }

        // 토큰 검증
        if (token == null || !token.startsWith("Bearer ")) {
            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            resp.setContentType("application/json; charset=UTF-8");
            resp.getWriter().write("{\"code\":\"UNAUTHORIZED\",\"message\":\"인증이 필요합니다\"}");
            return; // chain.doFilter를 호출하지 않음 → 요청이 여기서 멈춤!
        }

        chain.doFilter(request, response);
    }
}

5.5 FilterRegistrationBean (세밀한 제어)

@Component로 등록하면 모든 URL에 적용된다. 특정 URL에만 적용하려면 FilterRegistrationBean을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<LoggingFilter> loggingFilter() {
        FilterRegistrationBean<LoggingFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new LoggingFilter());
        registration.addUrlPatterns("/api/*");     // /api/** 에만 적용
        registration.setOrder(1);
        return registration;
    }

    @Bean
    public FilterRegistrationBean<AuthenticationFilter> authFilter() {
        FilterRegistrationBean<AuthenticationFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new AuthenticationFilter());
        registration.addUrlPatterns("/api/*");
        registration.setOrder(2);                  // 로깅 다음에 인증
        return registration;
    }
}

6. Interceptor (HandlerInterceptor)

6.1 Interceptor란?

Interceptor는 Spring MVC 레벨에서 동작한다. DispatcherServlet이 Controller를 호출하기 전/후에 실행된다. Filter와 달리 Spring Bean을 주입받을 수 있다.

6.2 HandlerInterceptor 인터페이스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface HandlerInterceptor {

    // Controller 실행 전 (true 반환 시 계속 진행, false면 중단)
    default boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {
        return true;
    }

    // Controller 실행 후, View 렌더링 전
    default void postHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            ModelAndView modelAndView) throws Exception {
    }

    // 요청 완료 후 (항상 실행, 예외 발생해도 실행)
    default void afterCompletion(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler,
                                 Exception ex) throws Exception {
    }
}

6.3 Interceptor 동작 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────────────┐
│                Interceptor 실행 시점                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  DispatcherServlet                                                  │
│       │                                                             │
│       ▼                                                             │
│  ① preHandle()    ── true 반환 시 계속 진행                        │
│       │               false 반환 시 여기서 중단                     │
│       ▼                                                             │
│  ② Controller 실행 (Handler)                                       │
│       │                                                             │
│       ▼                                                             │
│  ③ postHandle()   ── Controller 정상 완료 후 실행                  │
│       │               예외 발생 시 실행되지 않음!                    │
│       ▼                                                             │
│  ④ View 렌더링 (또는 JSON 변환)                                    │
│       │                                                             │
│       ▼                                                             │
│  ⑤ afterCompletion() ── 항상 실행 (예외가 발생해도)                │
│                          리소스 정리에 적합                          │
│                                                                     │
│  Interceptor 여러 개일 때:                                          │
│  preHandle:       1 → 2 → 3 (순방향)                              │
│  postHandle:      3 → 2 → 1 (역방향)                              │
│  afterCompletion: 3 → 2 → 1 (역방향)                              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

6.4 Interceptor 구현 예시

로그인 체크 Interceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j
@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {

    private final TokenProvider tokenProvider;  // Spring Bean 주입 가능!

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        String token = request.getHeader("Authorization");

        if (token == null || !tokenProvider.isValid(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json; charset=UTF-8");
            response.getWriter().write(
                    "{\"code\":\"UNAUTHORIZED\",\"message\":\"로그인이 필요합니다\"}");
            return false; // Controller에 도달하지 않음
        }

        // 인증된 사용자 정보를 request에 저장
        Long userId = tokenProvider.getUserId(token);
        request.setAttribute("userId", userId);

        return true; // Controller로 진행
    }
}

API 실행 시간 측정 Interceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
@Component
public class ExecutionTimeInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        request.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {

        long startTime = (long) request.getAttribute("startTime");
        long duration = System.currentTimeMillis() - startTime;

        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            log.info("[{}#{}] {}ms - status {}",
                    method.getBeanType().getSimpleName(),
                    method.getMethod().getName(),
                    duration,
                    response.getStatus());
        }

        // 느린 API 경고
        if (duration > 3000) {
            log.warn("[SLOW API] {} {} took {}ms",
                    request.getMethod(), request.getRequestURI(), duration);
        }
    }
}

6.5 Interceptor 등록

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final LoginCheckInterceptor loginCheckInterceptor;
    private final ExecutionTimeInterceptor executionTimeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 실행 시간 측정: 모든 API에 적용
        registry.addInterceptor(executionTimeInterceptor)
                .addPathPatterns("/api/**")
                .order(1);

        // 로그인 체크: 인증이 필요한 API에만 적용
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns(
                        "/api/login",
                        "/api/signup",
                        "/api/public/**",
                        "/api/health"
                )
                .order(2);
    }
}

7. Filter vs Interceptor 비교

7.1 핵심 차이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────────┐
│                Filter vs Interceptor 비교                            │
├──────────────────┬──────────────────┬───────────────────────────────┤
│                  │     Filter       │     Interceptor               │
├──────────────────┼──────────────────┼───────────────────────────────┤
│ 소속             │ Servlet Container│ Spring MVC                    │
│ 스펙             │ Java EE 표준     │ Spring 자체 스펙              │
│ 실행 시점        │ Servlet 전/후    │ Controller 전/후              │
│ Spring Bean 주입 │ △ (방법은 있음)  │ ✅ 자유롭게 가능              │
│ 요청/응답 조작   │ ✅ 가능 (교체도) │ △ (제한적)                    │
│ 예외 처리        │ try-catch 직접   │ @ControllerAdvice 사용 가능   │
│ URL 패턴         │ Servlet 패턴     │ Spring 패턴 (Ant/Path)        │
│ 대상             │ 모든 요청        │ DispatcherServlet 경유 요청만 │
├──────────────────┼──────────────────┼───────────────────────────────┤
│ 적합한 용도      │                  │                               │
│                  │ ● 인코딩 설정    │ ● 로그인/권한 체크            │
│                  │ ● XSS 방어      │ ● API 실행 시간 측정          │
│                  │ ● CORS 처리     │ ● 컨트롤러별 공통 로직        │
│                  │ ● 요청 본문 로깅 │ ● 감사 로그 (누가 어떤 API)  │
│                  │ ● Spring Security│ ● API 버전 체크              │
└──────────────────┴──────────────────┴───────────────────────────────┘

7.2 선택 기준

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────────────────────────┐
│                    선택 가이드                                        │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  "Spring Bean이 필요한가?"                                          │
│    ├── YES → Interceptor (Service, Repository 주입 가능)           │
│    └── NO  → 둘 다 가능                                            │
│                                                                     │
│  "Request/Response 자체를 교체·조작해야 하는가?"                    │
│    ├── YES → Filter (Wrapper로 request body 캐싱 등)               │
│    └── NO  → 둘 다 가능                                            │
│                                                                     │
│  "모든 요청(정적 리소스 포함)에 적용해야 하는가?"                    │
│    ├── YES → Filter (Servlet Container 레벨)                       │
│    └── NO  → Interceptor (DispatcherServlet 경유만)                │
│                                                                     │
│  "Spring Security를 쓰고 있는가?"                                   │
│    └── Spring Security의 인증/인가는 Filter 기반                   │
│        → SecurityFilterChain으로 처리                               │
│                                                                     │
│  실무 경험칙:                                                       │
│    보안·인코딩·CORS → Filter                                       │
│    비즈니스 관련 공통 로직 → Interceptor                             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

8. 전체 요청 흐름 총정리

8.1 요청부터 응답까지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
┌─────────────────────────────────────────────────────────────────────┐
│          Spring Boot 웹 요청의 전체 흐름                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Client (Browser / App)                                             │
│       │                                                             │
│       │ HTTP Request                                                │
│       ▼                                                             │
│  ┌─ Servlet Container (내장 Tomcat) ──────────────────────────┐    │
│  │                                                             │    │
│  │  ① TCP 연결 수립, HTTP 파싱                                 │    │
│  │  ② HttpServletRequest / Response 객체 생성                  │    │
│  │  ③ 스레드 풀에서 스레드 할당                                 │    │
│  │                                                             │    │
│  │  ④ Filter Chain 실행                                        │    │
│  │     EncodingFilter → CorsFilter → SecurityFilter            │    │
│  │     (전처리 → chain.doFilter → 후처리)                      │    │
│  │                         │                                   │    │
│  │  ┌─ Spring MVC ─────────┴──────────────────────────┐       │    │
│  │  │                                                  │       │    │
│  │  │  ⑤ DispatcherServlet.doDispatch()               │       │    │
│  │  │                                                  │       │    │
│  │  │  ⑥ Interceptor.preHandle()                      │       │    │
│  │  │     LoginCheckInterceptor → ExecutionTimeInterceptor     │    │
│  │  │                         │                        │       │    │
│  │  │  ⑦ HandlerMapping → 적절한 Controller 찾기      │       │    │
│  │  │                         │                        │       │    │
│  │  │  ⑧ HandlerAdapter → Controller 메서드 실행      │       │    │
│  │  │     - 파라미터 바인딩 (@RequestBody, @PathVariable)      │    │
│  │  │     - Validation (@Valid)                        │       │    │
│  │  │                         │                        │       │    │
│  │  │  ⑨ Controller → Service → Repository            │       │    │
│  │  │                         │                        │       │    │
│  │  │  ⑩ Interceptor.postHandle()                     │       │    │
│  │  │                         │                        │       │    │
│  │  │  ⑪ HttpMessageConverter (객체 → JSON 변환)      │       │    │
│  │  │                         │                        │       │    │
│  │  │  ⑫ Interceptor.afterCompletion()                │       │    │
│  │  │                                                  │       │    │
│  │  └──────────────────────────────────────────────────┘       │    │
│  │                                                             │    │
│  │  ⑬ Filter Chain 후처리 (역순)                               │    │
│  │                                                             │    │
│  └─────────────────────────────────────────────────────────────┘    │
│       │                                                             │
│       │ HTTP Response                                               │
│       ▼                                                             │
│  Client                                                             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

8.2 예외 발생 시 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────────┐
│                예외 발생 시 처리 흐름                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Service에서 예외 발생!                                              │
│       │                                                             │
│       ▼                                                             │
│  Controller까지 전파                                                │
│       │                                                             │
│       ▼                                                             │
│  Interceptor.postHandle() ← 실행 안 됨! (예외 발생 시 스킵)        │
│       │                                                             │
│       ▼                                                             │
│  @ControllerAdvice (@ExceptionHandler) ← 여기서 처리               │
│       │                                                             │
│       ▼                                                             │
│  Interceptor.afterCompletion() ← 실행됨! (예외 정보 포함)          │
│       │                                                             │
│       ▼                                                             │
│  Filter 후처리 ← 실행됨                                            │
│       │                                                             │
│       ▼                                                             │
│  에러 응답 → Client                                                │
│                                                                     │
│  주의: postHandle은 예외 시 스킵, afterCompletion은 항상 실행       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

9. Spring Security와 Filter

Spring Security는 Filter 기반으로 동작한다. DelegatingFilterProxy를 통해 Spring Bean으로 등록된 Security Filter들이 Servlet Filter Chain에 삽입된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌─────────────────────────────────────────────────────────────────────┐
│              Spring Security Filter Chain                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Servlet Filter Chain:                                              │
│                                                                     │
│  EncodingFilter                                                     │
│       │                                                             │
│  DelegatingFilterProxy (Spring Security 진입점)                     │
│       │                                                             │
│  ┌── SecurityFilterChain ──────────────────────────────────┐       │
│  │                                                          │       │
│  │  SecurityContextPersistenceFilter                        │       │
│  │       │                                                  │       │
│  │  CorsFilter                                              │       │
│  │       │                                                  │       │
│  │  UsernamePasswordAuthenticationFilter                    │       │
│  │       │                                                  │       │
│  │  BearerTokenAuthenticationFilter (JWT)                   │       │
│  │       │                                                  │       │
│  │  ExceptionTranslationFilter                              │       │
│  │       │                                                  │       │
│  │  FilterSecurityInterceptor (인가 처리)                   │       │
│  │                                                          │       │
│  └──────────────────────────────────────────────────────────┘       │
│       │                                                             │
│  DispatcherServlet                                                  │
│       │                                                             │
│  (Interceptor → Controller ...)                                     │
│                                                                     │
│  Spring Security의 인증/인가는 Controller에 도달하기 전에            │
│  Filter 레벨에서 처리된다                                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Spring Security 설정 (Spring Boot 3.x)
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/login", "/api/signup").permitAll()
                        .requestMatchers("/api/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

10. 실무 활용 패턴

10.1 Request Body 캐싱 Filter

HttpServletRequest의 Body(InputStream)는 한 번만 읽을 수 있다. 로깅용으로 Body를 읽으면 Controller에서 @RequestBody를 읽지 못한다. Wrapper로 해결한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Request Body를 캐싱하는 Wrapper
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    private final byte[] cachedBody;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        this.cachedBody = request.getInputStream().readAllBytes();
    }

    @Override
    public ServletInputStream getInputStream() {
        return new CachedBodyServletInputStream(cachedBody);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    public String getBody() {
        return new String(cachedBody);
    }
}

// Filter에서 Wrapper 적용
@Component
@Order(0)
public class RequestBodyCachingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;

        if ("POST".equalsIgnoreCase(req.getMethod())
                || "PUT".equalsIgnoreCase(req.getMethod())) {
            CachedBodyHttpServletRequest wrappedRequest =
                    new CachedBodyHttpServletRequest(req);
            chain.doFilter(wrappedRequest, response); // Wrapper로 교체!
        } else {
            chain.doFilter(request, response);
        }
    }
}

10.2 MDC(Mapped Diagnostic Context)를 활용한 요청 추적

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
@Order(1)
public class MdcFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        try {
            String requestId = UUID.randomUUID().toString().substring(0, 8);
            MDC.put("requestId", requestId);

            HttpServletRequest req = (HttpServletRequest) request;
            MDC.put("method", req.getMethod());
            MDC.put("uri", req.getRequestURI());

            chain.doFilter(request, response);
        } finally {
            MDC.clear(); // 반드시 정리 (스레드 풀 재사용 때문)
        }
    }
}
1
2
3
<!-- logback-spring.xml에서 requestId 출력 -->
<pattern>[%d] [%X{requestId}] [%thread] %-5level %logger - %msg%n</pattern>
<!-- 출력: [2026-03-24] [a1b2c3d4] [http-nio-8080-exec-1] INFO c.m.UserService - 사용자 조회 -->

모든 로그에 requestId가 남으므로 하나의 요청에 대한 전체 로그를 추적할 수 있다.


면접 예상 질문 & 답변

Q1. Servlet의 생명주기를 설명해주세요.

Servlet Container(Tomcat)가 시작되면 Servlet 클래스를 로딩하고 인스턴스를 하나만 생성합니다(싱글톤). 이후 init() 메서드가 한 번 호출되어 초기화됩니다.

클라이언트 요청이 올 때마다 스레드 풀에서 스레드를 할당받아 service() 메서드가 호출되고, HTTP 메서드에 따라 doGet(), doPost() 등이 실행됩니다. Servlet 인스턴스는 여러 스레드가 공유하므로 멤버 변수에 상태를 저장하면 Thread-Safety 문제가 발생합니다.

Container가 종료되면 destroy() 메서드가 호출되어 리소스를 정리합니다.

Q2. Filter와 Interceptor의 차이를 설명해주세요.

Filter는 Servlet Container 레벨에서 동작하며, 요청이 DispatcherServlet에 도달하기 전/후에 실행됩니다. Java EE 표준 스펙이고, Request/Response 자체를 교체할 수 있습니다.

Interceptor는 Spring MVC 레벨에서 동작하며, DispatcherServlet이 Controller를 호출하기 전/후에 실행됩니다. Spring Bean을 자유롭게 주입받을 수 있고, excludePathPatterns로 세밀하게 URL을 제어할 수 있습니다.

실무에서는 인코딩, CORS, 보안(Spring Security)은 Filter로, 로그인 체크, API 실행 시간 측정 등 비즈니스 관련 공통 로직은 Interceptor로 처리합니다.

Q3. DispatcherServlet의 역할을 설명해주세요.

DispatcherServlet은 Spring MVC의 프론트 컨트롤러로, 모든 HTTP 요청을 받아서 적절한 Controller에 위임합니다.

내부적으로 HandlerMapping으로 URL에 맞는 Controller를 찾고, HandlerAdapter로 메서드를 실행하며, 결과를 HttpMessageConverter(JSON 변환)나 ViewResolver(뷰 렌더링)를 통해 응답으로 만듭니다. 이 과정에서 파라미터 바인딩, Validation, 예외 처리 등이 함께 동작합니다.

DispatcherServlet도 결국 HttpServlet을 상속한 Servlet이며, Spring Boot가 시작될 때 자동으로 생성되어 / 경로에 매핑됩니다.

Q4. Spring MVC에서 요청이 처리되는 전체 흐름을 설명해주세요.

클라이언트 요청이 오면 먼저 Servlet Container(Tomcat) 가 TCP 연결을 수립하고 HttpServletRequest/Response 객체를 생성합니다.

그 다음 Filter Chain을 거칩니다. 인코딩, CORS, Spring Security 등이 여기서 처리됩니다.

Filter를 통과하면 DispatcherServlet에 도달하고, DispatcherServlet은 Interceptor의 preHandle을 실행합니다.

이후 HandlerMapping → HandlerAdapter → Controller → Service → Repository 순서로 비즈니스 로직이 처리됩니다.

Controller가 반환한 객체는 HttpMessageConverter가 JSON으로 변환하고, Interceptor의 postHandle, afterCompletion을 거쳐, Filter 후처리를 지나 최종 응답이 클라이언트에 전달됩니다.

Q5. Filter에서 chain.doFilter()를 호출하지 않으면 어떻게 되나요?

chain.doFilter()는 다음 Filter 또는 Servlet으로 요청을 넘기는 역할입니다. 호출하지 않으면 요청이 그 Filter에서 멈추고 이후의 Filter, DispatcherServlet, Controller 모두 실행되지 않습니다.

이 특성을 이용해 인증 Filter에서 토큰이 유효하지 않으면 chain.doFilter()를 호출하지 않고 즉시 401 응답을 내보내는 패턴을 자주 사용합니다.

Q6. Interceptor의 preHandle, postHandle, afterCompletion 차이는?

preHandle은 Controller 실행 에 호출됩니다. false를 반환하면 요청 처리가 중단됩니다. 인증 체크에 주로 사용합니다.

postHandle은 Controller 정상 실행 , View 렌더링 에 호출됩니다. Controller에서 예외가 발생하면 실행되지 않습니다.

afterCompletion은 요청 처리가 완전히 끝난 후 호출됩니다. 예외 발생 여부와 무관하게 항상 실행되므로 리소스 정리에 적합합니다. 예외 정보도 파라미터로 받을 수 있습니다.

Q7. Spring Security는 Filter 기반인가요, Interceptor 기반인가요?

Spring Security는 Filter 기반입니다. DelegatingFilterProxy를 통해 Servlet Filter Chain에 SecurityFilterChain이 삽입됩니다.

인증(Authentication)과 인가(Authorization)가 DispatcherServlet에 도달하기 전에 Filter 레벨에서 처리되므로, 인증되지 않은 요청은 Controller까지 도달하지 않습니다.

이 때문에 Spring Security의 인증 로직에서는 Service나 Repository 같은 Spring Bean을 직접 주입받기보다, UserDetailsService 같은 인터페이스를 통해 간접적으로 접근합니다.


마무리

웹 요청이 Controller에 도달하기까지 여러 계층을 거친다. 이 전체 흐름을 이해하면 “어디에 공통 로직을 넣을 것인가”를 올바르게 판단할 수 있다.

  • Servlet: HTTP 요청을 처리하는 Java 표준, 싱글톤으로 동작
  • Servlet Container: Servlet 생명주기 관리, 스레드 풀, HTTP 파싱
  • DispatcherServlet: 프론트 컨트롤러, 모든 요청을 받아 적절한 Controller로 위임
  • Filter: Servlet Container 레벨, Request/Response 조작 가능, 보안·인코딩에 적합
  • Interceptor: Spring MVC 레벨, Bean 주입 가능, 비즈니스 공통 로직에 적합
  • 전체 흐름: Client → Tomcat → Filter → DispatcherServlet → Interceptor → Controller

“Filter와 Interceptor 차이”는 면접 단골이지만, 단순히 차이만 외우지 말고 왜 그 차이가 생기는지(소속 계층의 차이)를 이해하면 후속 질문에도 자연스럽게 답할 수 있다.