1강. 클라이언트 요청은 Controller 전에 FilterChain을 지난다

한 줄 결론

Spring Security는 Controller 내부 기능이 아니라 Servlet FilterChain 위에서 동작하므로, 인증/인가 문제는 Controller에 도착하기 전 단계에서 결정될 수 있다.

3줄 요약

  • HTTP 요청의 첫 도착지는 Controller가 아니라 Servlet Container다.
  • Servlet Container는 요청에 맞는 FilterChain을 실행하고, Spring Security는 이 구간에서 인증/인가, CORS, CSRF, 예외 처리를 수행한다.
  • Controller breakpoint가 걸리지 않는 401/403/302/CORS 문제는 먼저 Security FilterChain 통과 여부부터 확인해야 한다.

체크포인트

  • Controller가 요청의 첫 번째 도착지가 아닌 이유를 설명할 수 있는가?
  • FilterFilterChain이 요청을 다음 단계로 넘기는 방식을 설명할 수 있는가?
  • chain.doFilter(request, response)를 호출하지 않으면 어떤 일이 생기는지 말할 수 있는가?
  • 401, 403, 302, CORS 오류를 Controller보다 앞단에서 의심해야 하는 이유를 설명할 수 있는가?
  • Security 적용 전후 요청 흐름을 비교할 수 있는가?

전체 흐름

  1. 흔히 생각하는 MVC 요청 흐름의 한계를 확인한다.
  2. 실제 요청이 Servlet Container와 FilterChain을 먼저 지나는 구조를 본다.
  3. Filter와 chain.doFilter()가 요청 흐름을 제어하는 방식을 이해한다.
  4. Spring Security가 FilterChain 어디에 끼어드는지 확인한다.
  5. 401/403뿐 아니라 302, CORS, CSRF 403, 404, 405를 함께 해석하는 기준을 잡는다.
  6. Controller breakpoint가 걸리지 않을 때 요청이 어디서 멈췄는지 추적하는 사고법을 정리한다.

분리 후보

현재 문서는 Controller 전단의 요청 흐름을 한 번에 잡는 목적이므로 유지한다. 다만 길이가 부담되면 Servlet Container와 FilterChain 기본 흐름, Spring Security FilterChain과 인증/인가, 401/403/302/CORS 문제 추적법의 3개 문서로 나눌 수 있다.

확인 질문

아래 4개에 답할 수 있으면 넘어가면 된다.

1. Controller는 요청의 첫 번째 도착지인가?
 
2. Filter에서 chain.doFilter(request, response)를 호출하지 않으면
   어떤 일이 발생하는가?
 
3. Controller에 breakpoint를 걸었는데 401 또는 403이 나면서 멈추지 않았다.
   이때 가장 먼저 의심해야 하는 구간은 어디인가?
 
4. 보호 API가 예상대로 동작하지 않을 때
   왜 401/403뿐 아니라 302, CORS, CSRF 403, 404, 405도 같이 봐야 하는가?

이 문서의 결론

이번 문서의 결론은 아래와 같다.

클라이언트 요청은 Controller로 바로 가지 않는다.
 
요청은 먼저 Servlet Container에 도착하고,
Servlet Container는 해당 요청에 적용될 FilterChain을 실행한다.
 
Spring Security는 이 FilterChain 구간에 끼어들어,
Controller에 도착하기 전에 인증/인가를 처리할 수 있다. 뿐만아니라, 예외 처리, CSRF, CORS 관련 처리를 수행할 수 있다.
 
따라서 401/403 등의 문제를 볼 때는 ( 대표적으로 401, 403이 가장 자주 보이지만, 상황에 따라 302, 404, 405, CORS 오류, CSRF 403, 500도 FilterChain 또는 Security 설정과 연결될 수 있다.)
보호 API가 예상대로 동작하지 않는 경우로, 
Controller 코드부터 보는 것이 아니라,
요청이 Security FilterChain을 통과했는지,
어느 필터에서 멈췄는지,
어떤 보안 규칙에 의해 응답이 결정됐는지부터 봐야 한다.

핵심은 Spring Security는 Controller 내부 기능이 아니라 Servlet Filter 위에서 동작한다는 점이다.

먼저 증상별 가능한 원인부터 짚어보자. 문서를 다 읽고 나면 모두 이해가 될 것이다.

증상가능한 원인
401토큰 없음, Bearer prefix 누락, JWT 만료, 서명 실패
403권한 부족, ROLE_ prefix 문제, CSRF token 누락
302API인데 form login이 켜져 로그인 페이지로 redirect
404requestMatcher는 통과했지만 실제 Controller 매핑 없음
405URL은 맞지만 HTTP method가 다름
CORS 오류브라우저 preflight 요청이 Security/CORS 설정에서 막힘
500Security 필터나 JWT 파싱 로직 내부 예외 처리 실패
Controller breakpoint 안 걸림FilterChain에서 이미 차단됨

결론에 도달해보자!

우리가 흔히 생각하는 요청 흐름

Spring MVC로 간단히 바라본 요청 흐름을 보통 이렇다.

클라이언트 요청
→ Controller
→ Service
→ Mapper / Repository
→ DB
→ 응답

예를 들어 이런 요청이 있다고 해보자.

GET /api/me
Authorization: Bearer access-token

그러면 자연스럽게 아래 Controller가 바로 실행될 것처럼 생각한다.

예시

@GetMapping("/api/me")
public UserResponse me() {
    return userService.getCurrentUser();
}

하지만 실제로는 요청이 Controller로 바로 가지 않는다.

Controller는 중요한 처리 지점이지만, 요청의 첫 번째 도착지는 아니다.


실제 요청 흐름

Spring Boot + Spring MVC 기준으로 보면 더 정확한 흐름은 아래와 같다.

Client
→ Servlet Container
→ FilterChain
→ Spring Security FilterChain
→ DispatcherServlet
→ Controller
→ Service
→ Mapper / Repository
→ DB
→ Response

조금 더 풀어 쓰면 이렇다.

1. 클라이언트가 HTTP 요청을 보낸다.
2. 서버의 Servlet Container가 요청을 받는다.
3. Servlet Container가 HttpServletRequest, HttpServletResponse를 만든다.
4. 요청에 적용될 Filter들을 순서대로 실행한다.
5. Spring Security도 이 Filter 구간에서 실행된다.
6. 보안 처리를 통과하면 DispatcherServlet으로 요청이 넘어간다.
7. DispatcherServlet이 어떤 Controller 메서드를 실행할지 찾는다.
8. Controller가 실행된다.
9. 응답은 다시 Filter들을 거꾸로 지나 클라이언트로 돌아간다.

한 마디로

Controller는 요청 처리의 핵심이지만, 요청의 시작점은 아니다. Spring Security는 Controller보다 앞에서 요청을 검사할 수 있다.


Servlet Container란 무엇인가?

우리가 작성한 Controller는 그냥 Java 객체다.

브라우저가 보낸 HTTP 요청이 직접 Java 메서드를 호출할 수는 없다. 중간에서 HTTP 요청을 받고, Java 웹 애플리케이션이 처리할 수 있는 형태로 바꿔주는 실행 환경이 필요하다.

그 역할을 하는 것이 Servlet Container다.

대표적으로 아래와 같은 것들이 있다.

Tomcat
Jetty
Undertow

Spring Boot 웹 애플리케이션을 만들면 보통 내장 Tomcat이 실행된다.

Servlet Container는 대략 이런 일을 한다.

HTTP 요청 받기
HttpServletRequest / HttpServletResponse 만들기
FilterChain 실행하기
DispatcherServlet 호출하기
응답을 HTTP 응답으로 변환해 돌려보내기

즉, Controller보다 먼저 요청을 받는 쪽은 Spring MVC Controller가 아니라 Servlet Container다.


DispatcherServlet은 무엇인가?

Spring MVC에서 Controller를 찾아 실행하는 중심 입구가 DispatcherServlet이다.

DispatcherServlet
= Spring MVC의 중앙 입구
= 요청 URL과 HTTP method를 보고 어떤 Controller 메서드를 실행할지 결정하는 객체

예를 들어 요청이 이렇게 들어왔다고 하자.

GET /api/users/1

그러면 DispatcherServlet은 내부적으로 이런 판단을 한다.

이 요청을 누가 처리하지?
→ UserController인가?
→ @GetMapping("/api/users/{id}")인가?
→ 그럼 이 메서드를 실행하자.

그래서 Spring MVC만 보면 흐름은 이렇다.

DispatcherServlet
→ HandlerMapping
→ Controller
→ Service
→ 응답

하지만 여기서 중요한 점이 있다.

DispatcherServlet도 FilterChain 뒤에 있다.

즉, 요청이 FilterChain에서 막히면 DispatcherServlet까지 가지 못한다.

그러면 당연히 Controller도 실행되지 않는다.


Filter는 왜 필요한가?

Controller마다 공통으로 해야 하는 일이 있다.

로그 남기기
인코딩 처리
CORS 처리
인증 확인
권한 확인
CSRF 확인
요청 추적 ID 부여
압축
예외 처리

이런 코드를 모든 Controller에 직접 넣으면 코드가 지저분해진다.

좋지 않은 예시

@GetMapping("/api/me")
public UserResponse me(HttpServletRequest request) {
    // 1. Authorization header 확인
    // 2. 토큰 추출
    // 3. 토큰 만료 확인
    // 4. 사용자 권한 확인
    // 5. CORS 확인
    // 6. 실제 비즈니스 로직
    return userService.getCurrentUser();
}

이러면 모든 Controller가 보안 코드로 오염된다.

그래서 Controller 앞에 공통 처리 계층을 둔다.

그게 Filter다.

Filter
= Controller에 도착하기 전 요청을 검사하거나 가공할 수 있는 객체

Filter는 요청을 다음 단계로 넘길 수도 있고, 중간에 응답을 직접 만들어서 요청을 끝낼 수도 있다.


FilterChain은 무엇인가?

Filter는 하나만 있는 게 아니다.

여러 Filter가 순서대로 실행된다.

Client
→ Filter A
→ Filter B
→ Filter C
→ DispatcherServlet
→ Controller

그리고 응답이 돌아올 때는 반대 방향으로 돌아온다.

Controller
→ DispatcherServlet
→ Filter C 후처리
→ Filter B 후처리
→ Filter A 후처리
→ Client

이 전체 줄이 FilterChain이다.

FilterChain
= 현재 요청에 적용되는 Filter들의 실행 순서

chain.doFilter()의 의미

Filter 코드를 보면 자주 나오는 코드가 있다.

Filter 구조 예시

public void doFilter(
    ServletRequest request,
    ServletResponse response,
    FilterChain chain
) throws IOException, ServletException {
 
    // 요청 전처리
    System.out.println("요청 들어옴");
 
    chain.doFilter(request, response);
 
    // 응답 후처리
    System.out.println("응답 나감");
}

핵심은 이 줄이다.

chain.doFilter(request, response);

이 말은 아래 의미다.

나는 내 할 일을 했다.
이제 다음 Filter 또는 최종 DispatcherServlet 쪽으로 요청을 넘기겠다.

반대로 chain.doFilter()를 호출하지 않으면 어떻게 될까?

요청은 다음 단계로 가지 않는다.
DispatcherServlet도 실행되지 않는다.
Controller도 실행되지 않는다.

예를 들어 토큰이 없을 때 Filter가 직접 401 응답을 만들 수 있다.

요청을 중간에 끊는 예시

if (token == null) {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return;
}
 
chain.doFilter(request, response);

이 경우 요청은 Controller까지 가지 않는다.


Spring Security는 어디에 끼어드는가?

Spring Security는 Controller 안쪽에서 동작하는 기능이 아니다.

Spring Security는 Servlet FilterChain에 들어간다.

큰 흐름은 이렇다.

Client
→ Servlet Container
→ Servlet FilterChain
→ Spring Security 관련 Filter들
→ DispatcherServlet
→ Controller

조금 더 Spring Security스럽게 쓰면 이렇다.

Client
→ Servlet Container
→ DelegatingFilterProxy
→ FilterChainProxy
→ SecurityFilterChain
→ Spring Security Filter들
→ DispatcherServlet
→ Controller

용어는 문서를 읽다보면 자연스레 익혀질 것이다. 아래만 짚고 넘어가자.

용어지금 단계의 이해
DelegatingFilterProxyServlet Container와 Spring Bean 세계를 연결하는 입구
FilterChainProxySpring Security의 중앙 필터
SecurityFilterChain현재 요청에 적용될 Spring Security 필터 목록
Spring Security Filter들인증, 인가, 예외 처리, CSRF 등을 수행하는 필터들

핵심 : Spring Security는 Controller 앞에서 이런 질문을 먼저 던진다.

이 요청은 인증이 필요한가?
토큰이 있는가?
토큰이 유효한가?
현재 사용자는 누구인가?
권한은 무엇인가?
이 URL에 접근 가능한가?
CSRF 토큰이 필요한 요청인가?
CORS preflight 요청인가?

이 질문에서 막히면 요청은 Controller까지 가지 않는다.


401/403이 대표적이지만…

보호 API가 예상대로 동작하지 않을 때의 여러 증상은 아래와 같다.

증상대략적인 의미FilterChain / Security 관점에서 볼 것
401 Unauthorized인증 실패 또는 인증 필요토큰 없음, Bearer prefix 누락, JWT 만료, 서명 실패
403 Forbidden권한 부족 또는 접근 거부권한 부족, ROLE_ prefix 문제, CSRF token 누락
302 Redirect다른 URL로 리다이렉트API인데 form login이 켜져 로그인 페이지로 이동하는지
404 Not Found매핑된 자원 없음Security는 통과했지만 Controller 매핑이 없는지, matcher가 예상과 다른지
405 Method Not AllowedURL은 맞지만 method가 다름GET으로 열었는데 실제 API는 POST인지
CORS 오류브라우저가 요청 차단preflight OPTIONS 요청이 Security/CORS 설정에서 허용되는지
CSRF 403CSRF 보호에 의해 차단POST/PUT/DELETE 요청에 CSRF token이 필요한지
500 Internal Server Error서버 내부 예외JWT 파싱, Security filter, custom handler 내부 예외 여부

즉, 증상만으로 Controller 전에 요청이 끝났음을 알 수 있고, 대략적으로 파악도 가능하다.

실제 예시와 함께 살펴보자.

예시 1. 토큰 없음 (인증 실패)

요청이 이렇게 들어왔다고 하자.

GET /api/me

그런데 Security 설정이 아래와 같다면:

.anyRequest().authenticated()

흐름은 이렇다.

GET /api/me
→ Servlet Container
→ FilterChain
→ Spring Security FilterChain
→ 인증 정보 없음
→ 인증 필요
→ 401 Unauthorized
→ Controller 도착 안 함

그래서 401이다.

앞서 설명했듯, Controller는 실행되지 않는다. Security FilterChain에서 401과 관련된, 인증 정보에 관련된 코드를 디버깅 하면 된다.

실행되지 않는 Controller

@GetMapping("/api/me")
public UserResponse me() {
    // 여기까지 오지 않음
    return userService.getCurrentUser();
}

예시 2. 권한 없음

요청이 이렇게 들어왔다고 하자.

GET /api/admin/report
Authorization: Bearer ROLE_USER_TOKEN

Security 설정은 아래와 같다.

.requestMatchers("/api/admin/**").hasRole("ADMIN")

흐름은 이렇다.

GET /api/admin/report
→ Servlet Container
→ FilterChain
→ Spring Security FilterChain
→ JWT 검증 성공
→ Authentication 생성 성공
→ 현재 권한: ROLE_USER
→ 필요한 권한: ROLE_ADMIN
→ AuthorizationFilter에서 거부
→ 403 Forbidden
→ Controller 도착 안 함

이 경우는 인증이 안 된 게 아니고, 권한이 부족하다.

그래서 403이 된다.


예시 3. API인데 302가 나오는 경우

REST API를 만들고 있는데, 요청 결과가 401이 아니라 로그인 페이지로 redirect 되는 경우가 있다.

GET /api/me

응답이 이런 식으로 올 수 있다.

302 Found
Location: /login

이 경우 흔한 원인은 이런 설정이다.

formLogin이 켜져 있음
인증 안 된 사용자를 로그인 페이지로 보내는 기본 흐름이 동작함

API 서버에서는 보통 JSON 응답이나 401을 기대한다.

그런데 form login 흐름이 살아 있으면 브라우저 기준 로그인 페이지로 redirect될 수 있다.

즉, 이것도 Controller 문제가 아닐 수 있다.

Security의 인증 실패 처리 방식 문제일 수 있다.

예시 4. CORS 오류

프론트엔드에서 API를 호출했는데 브라우저 콘솔에 CORS 오류가 뜬다고 하자.

Access to fetch at 'http://localhost:8080/api/me'
from origin 'http://localhost:3000'
has been blocked by CORS policy

이때 서버 Controller에 breakpoint를 걸어도 안 멈출 수 있다.

왜냐하면 브라우저가 실제 요청 전에 preflight 요청을 먼저 보낼 수 있기 때문이다.

OPTIONS /api/me
Origin: http://localhost:3000
Access-Control-Request-Method: GET

OPTIONS 요청이 Security/CORS 설정에서 막히면 실제 GET /api/me는 보내지지 않을 수도 있다.

흐름은 이런 식이다.

Browser
→ OPTIONS /api/me preflight
→ Security/CORS 설정에서 거부
→ 브라우저가 실제 GET 요청 차단
→ Controller 도착 안 함

따라서 CORS 오류도 단순 Controller 문제가 아니다.


예시 5. CSRF 때문에 403이 나는 경우

Spring Security에서 CSRF 보호가 켜져 있는데, 아래 요청을 보낸다고 하자.

POST /api/auth/logout
Cookie: JSESSIONID=...

CSRF token이 없다면 403이 날 수 있다.

POST /api/auth/logout
→ Servlet Container
→ FilterChain
→ Spring Security FilterChain
→ CsrfFilter
→ CSRF token 없음
→ 403 Forbidden
→ Controller 도착 안 함

여기서도 403이지만, 권한 부족 때문이 아닐 수 있다.

403이라고 해서 항상 ROLE 부족은 아니다.
CSRF 보호 때문에 403일 수도 있다.

예시 6. 404/405도 같이 봐야 하는 이유

보안 문제를 보다가 404나 405를 만날 수도 있다.

404

GET /api/user/me

그런데 실제 Controller는 이렇다.

@GetMapping("/api/users/me")
public UserResponse me() {
    return userService.getCurrentUser();
}

이 경우는 Security를 통과해도 Controller 매핑이 없다.

Security는 통과
→ DispatcherServlet 도착
→ 매핑되는 Controller 없음
→ 404

405

GET /api/auth/login

그런데 실제 Controller는 이렇다.

@PostMapping("/api/auth/login")
public TokenResponse login(@RequestBody LoginRequest request) {
    ...
}

이 경우 URL은 맞지만 method가 다르다.

URL은 맞음
HTTP method가 다름
→ 405 Method Not Allowed

그래서 보호 API가 안 될 때는 단순히 “보안 문제다”라고만 보면 안 된다.

FilterChain에서 막힌 건지,
DispatcherServlet까지 간 뒤 매핑에서 실패한 건지
구분해야 한다.

요청이 어디까지 갔는지 구분하는 사고법

문제가 생겼을 때는 이렇게 생각하면 된다.

1. 요청이 서버에 도착했는가?
2. CORS preflight에서 막혔는가?
3. Servlet FilterChain에 들어왔는가?
4. Spring Security FilterChain에서 막혔는가?
5. 인증 필터가 Authentication을 만들었는가?
6. AuthorizationFilter가 허용했는가?
7. DispatcherServlet까지 도착했는가?
8. Controller mapping이 되었는가?
9. Controller 내부 로직에서 터졌는가?

이 순서로 보면 막연한 디버깅이 줄어든다.


Controller breakpoint가 안 걸렸을 때

Controller에 breakpoint를 걸었는데 멈추지 않는다면, 다음을 의심해야 한다.

요청이 Controller까지 오기 전에 끝났을 수 있다.

먼저 볼 것들은 아래와 같다.

SecurityConfig
requestMatchers
permitAll / authenticated / hasRole
Authorization header 존재 여부
Bearer prefix 여부
JWT 만료 여부
JWT 서명 검증 여부
authority 매핑 여부
CSRF 설정
CORS 설정
formLogin redirect 여부
Spring Security DEBUG 로그

Tip

Controller breakpoint가 안 걸린다는 것은 요청이 안 왔다는 뜻이 아니다. 요청은 왔지만 FilterChain에서 끝났을 수 있다.


Spring Security 적용 전후 요청 흐름 비교

구분요청 흐름
Security 없이 단순하게 볼 때Client → DispatcherServlet → Controller → Service
Security 적용 후 실제 감각Client → Servlet Container → FilterChain → Spring Security FilterChain → DispatcherServlet → Controller
보안에서 차단되는 경우Client → Servlet Container → FilterChain → Spring Security FilterChain → 응답 종료
보안 통과 후 매핑 실패Client → Servlet Container → FilterChain → Spring Security FilterChain → DispatcherServlet → 404/405
보안 통과 후 정상 처리Client → Servlet Container → FilterChain → Spring Security FilterChain → DispatcherServlet → Controller → Service

자주 하는 오해

오해교정
요청은 Controller로 바로 간다실제로는 Servlet Container와 FilterChain을 먼저 지난다
401/403만 Security 문제다302, CORS, CSRF 403, 500도 Security 설정과 연결될 수 있다
403은 항상 권한 부족이다CSRF token 누락 때문에 403일 수도 있다
Controller breakpoint가 안 걸리면 요청이 안 온 것이다요청은 왔지만 FilterChain에서 끝났을 수 있다
JWT면 SecurityContextHolder를 안 쓴다JWT도 현재 요청 중에는 Authentication을 SecurityContextHolder에 저장한다
CORS 오류는 Controller에서 해결한다많은 경우 preflight가 Controller 전에 처리된다
404는 항상 URL 오타다requestMatcher와 Controller mapping 불일치도 함께 봐야 한다
302는 정상 응답이다API 서버에서는 formLogin redirect가 의도치 않은 설정일 수 있다

핵심 정리

웹 요청은 Controller로 바로 가지 않는다.
 
요청은 Servlet Container에 먼저 도착하고,
FilterChain을 통과한 뒤,
DispatcherServlet을 거쳐 Controller로 간다.
 
Spring Security는 이 FilterChain 구간에서 동작한다.
 
따라서 보호 API가 예상대로 동작하지 않을 때는
Controller 코드부터 보는 것이 아니라,
요청이 Security FilterChain에서 어떻게 처리됐는지부터 봐야 한다.
 
대표적인 증상은 401과 403이지만,
실전에서는 302 redirect, CORS 오류, CSRF 403, 404, 405, 500도 함께 봐야 한다.
 
중요한 것은 HTTP 상태 코드 하나를 외우는 것이 아니라,
요청이 Controller까지 도착했는지,
FilterChain에서 끝났는지,
DispatcherServlet 이후에서 실패했는지를 구분하는 것이다.