1. 문제 제기 (Introduction & Problem Statement)
- 관찰 현상 또는 질문:
Spring Security를 학습하다 보면 *"로그인한 사용자 정보를 어떻게 컨트롤러의 인자(@AuthenticationPrincipal)로 바로 받아오는 것일까?"* 하는 의문이 든다. 또한, 보안 설정이 적용되는 위치가 Servlet Filter인지, AOP인지 혼동하기 쉽다. 특히 *"URL 보안 검사는 헤더 검사와 무엇이 다른가?", *"ThreadLocal은 메모리 어디에 위치하는가?" 와 같은 근본적인 동작 원리에 대한 이해가 부족하면, 문제가 발생했을 때 디버깅하기 어렵다. - 탐구 목표:
본 아티클에서는 HTTP 요청이 서버에 도달하여 컨트롤러에 도착하기까지의 흐름을 분석하고, Servlet Filter, Interceptor, AOP의 명확한 역할 차이를 규명한다. 또한, Spring Security가 ThreadLocal을 활용하여 사용자 정보를 전역적으로 관리하는 매커니즘과 이를 안전하게 사용하는 방법을 심층 분석한다.
2. 기술 분석 및 핵심 원리 (Technical Deep Dive)
2-1. 요청 처리의 3단계 방어선: Filter vs Interceptor vs AOP
보안 로직이 어디서 실행되는지 이해하려면, 요청이 통과하는 레이어(Layer)를 구분해야 한다.
| 구분 | Servlet Filter | Handler Interceptor | Spring AOP |
|---|---|---|---|
| 위치 | Tomcat ↔ DispatcherServlet | DispatcherServlet ↔ Controller | Service/Controller 메서드 레벨 |
| 관할 | Web Container (Tomcat) | Spring MVC Context | Spring Core (Proxy) |
| 주요 용도 | Spring Security, CORS, 인코딩 | 로그인 세션 체크(구식), 로깅 | @Transactional, 메서드 보안 |
| 실행 시점 | 가장 먼저 실행됨 | 컨트롤러 호출 직전/직후 | 메서드 내부 진입 전/후 |
- 분석: Spring Security의 인증/인가(URL 보안)는 DispatcherServlet에 도달하기도 전, 즉 가장 바깥쪽인 Filter 단계에서 처리된다. 반면,
@PreAuthorize같은 세밀한 메서드 보안은 AOP를 통해 구현된다.
2-2. URL 보안과 메서드 보안의 동작 방식
1. URL 보안 (FilterChain)
Spring Security는 DelegatingFilterProxy라는 필터를 통해 서블릿 컨테이너(Tomcat)와 스프링 컨테이너를 연결한다.
- 질문: *"헤더나 JWT 검사만 하면 되지, 왜 URL 보안이 따로 있는가?"*
- 답변: 헤더 검사는 인증(Authentication, 내가 누구인가)을 확인하는 과정이다. URL 보안은 인가(Authorization, 내가 여기를 갈 수 있는가)를 확인하는 과정이다.
- 예: 사용자 A가 JWT를 가져왔다(인증 성공). 하지만
/admin/**경로로 접근하려 한다. 이때 URL 보안 설정(antMatchers)이 필터 레벨에서 이를 가로막는다.
2. 메서드 보안 (AOP & SpEL)@EnableMethodSecurity를 활성화하면 AOP 프록시가 동작한다.
- @PreAuthorize: 메서드 실행 전에 SpEL 표현식을 평가하여 접근을 허용/거부한다.
- @PostAuthorize: 메서드 실행 후에 반환값(
returnObject)을 검사하여 접근을 제어한다.
2-3. 사용자 식별의 핵심: SecurityContextHolder와 ThreadLocal
Spring Security가 사용자 정보를 "전역적"으로 관리하는 비결은 ThreadLocal에 있다.
1. ThreadLocal의 정체
- 정의: 오직 현재 쓰레드에서만 접근 가능한 고유한 저장소다.
- 메모리 위치: 자바의 쓰레드(Thread)도 결국 객체이므로 JVM Heap 영역에 생성된다.
ThreadLocal은 이 쓰레드 객체 내부에 있는 Map(ThreadLocalMap)을 통해 값을 관리한다. 물리적으로는 같은 Heap에 있지만, 논리적으로는 해당 쓰레드만 접근할 수 있도록 격리되어 있다.
2. 인증 객체의 흐름 (Lifecycle)
- 요청 진입 (Filter): 사용자가 요청을 보냄 → Security Filter가 헤더(쿠키/토큰)를 확인.
- 저장 (Authentication): 유효한 사용자라면
Authentication객체(UserDetail 포함)를 생성. - 등록 (SecurityContextHolder): 생성된 객체를
SecurityContextHolder에 저장.
- 이때 내부적으로
ThreadLocal에 저장되므로, 이후 Controller나 Service 어디에서든 인자로 넘기지 않아도 접근 가능.
사용 (Controller):
// 4. 주입: AuthenticationPrincipalArgumentResolver가 ThreadLocal에서 꺼내서 꽂아줌 @GetMapping("/") public String home(@AuthenticationPrincipal User user) { ... }
- 정리 (Clear): 요청 처리가 끝나면 Filter가 다시 동작하여
ThreadLocal을 비워준다(Clear).
- 주의: Tomcat은 쓰레드 풀(Thread Pool)을 사용하므로, 쓰레드를 재사용한다. 만약 비우지 않으면 다른 사용자가 이전 사용자의 정보를 조회하는 치명적인 사고가 발생할 수 있다.
3. 결론 및 고찰 (Conclusion & Takeaways)
- 핵심 요약:
- Spring Security의 거시적인 보안(URL 접근 제어)은 Servlet Filter 레벨에서, 미시적인 보안(메서드 실행 제어)은 AOP 레벨에서 동작한다.
- URL 보안은 '인증된 사용자'가 '허용된 경로'에 접근하는지를 가장 앞단에서 막아주는 문지기 역할을 한다.
ThreadLocal은 요청마다 할당되는 쓰레드만의 비밀 사물함이다. 이를 통해 우리는 컨트롤러의 모든 메서드에User객체를 파라미터로 넘기는 번거로움을 피할 수 있다.
- 기술적 통찰 및 나의 생각:
학습 전에는 단순히 "어노테이션을 붙이면 된다"고 생각했으나, 그 이면에는 Filter Chain의 가로채기와 ThreadLocal의 격리성이라는 정교한 메커니즘이 숨어 있었다. 특히 ThreadLocal이 JVM Heap에 존재하면서도 논리적으로 격리된다는 점, 그리고 쓰레드 풀 환경에서 반드시 초기화(Clear)가 필요하다는 점은 서버 안정성 측면에서 매우 중요한 포인트임을 깨달았다. - 향후 과제:
- 비동기 처리(
@Async)를 할 때, 새로운 쓰레드가 생성되면ThreadLocal의 정보는 어떻게 전달되는가? (DelegatingSecurityContextExecutor학습 필요) - JWT 기반의 무상태(Stateless) 아키텍처에서도
SecurityContext를 매 요청마다 생성하는데, 이것이 성능에 미치는 영향은 무엇인가?