# Spring Security 6.x - SecurityContextHolder 동작 원리 완벽 정리
## 🤔 왜 JWT 인증할 때 매번 setAuthentication()을 호출할까?
JWT 기반 인증 코드를 보면 항상 이런 패턴이 있다.
```java
SecurityContextHolder.getContext().setAuthentication(authentication);
왜 매 요청마다 이걸 해줘야 할까? 공식 문서를 기반으로 정리해본다.
Spring Security는 SecurityContextHolder가 어떻게 채워지는지 신경 쓰지 않는다. 값이 포함되어 있으면, 그것이 현재 인증된 사용자로 사용된다.
— Spring Security 공식 문서
SecurityContextHolder
└── SecurityContext
└── Authentication (현재 인증된 사용자 정보)
기본적으로 SecurityContextHolder는 ThreadLocal 객체를 사용하여 보안 컨텍스트를 저장한다. 이는 같은 스레드 내의 메서드들에서는 SecurityContext 객체를 직접 전달하지 않아도 항상 보안 컨텍스트에 접근할 수 있다는 것을 의미한다.
[Thread-1의 책상] [Thread-2의 책상] [Thread-3의 책상]
- 김철수 정보 - 이영희 정보 - 박민수 정보
stateless RESTful 웹 서비스와 같은 많은 다른 유형의 애플리케이션은 HTTP 세션을 사용하지 않고 매 요청마다 재인증한다.
— Spring Security Technical Overview
=== 요청 1: 김철수가 API 호출 ===
[Thread-1]
│
▼
┌─────────────────────────────────┐
│ JWT 필터 │
│ 1. JWT에서 "김철수" 정보 추출 │
│ 2. setAuthentication(김철수) │ ← set!
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Controller │
│ getAuthentication() → "김철수" │
└─────────────────────────────────┘
│
▼
[요청 끝 → SecurityContext 비워짐]
=== 요청 2: 이영희가 API 호출 ===
[Thread-1] ← 같은 스레드 재사용 가능 (Thread Pool)
│
▼
┌─────────────────────────────────┐
│ JWT 필터 │
│ 1. JWT에서 "이영희" 정보 추출 │
│ 2. setAuthentication(이영희) │ ← 다시 set!
└─────────────────────────────────┘
Thread Pool 방식이라 같은 스레드가 재사용될 수 있지만, 요청 끝날 때 clear 되므로 매번 새로 set 해야 한다!
| 버전 | Load | Save | Clear |
|---|---|---|---|
| 5.x | SecurityContextPersistenceFilter | SecurityContextPersistenceFilter | SecurityContextPersistenceFilter |
| 6.x | SecurityContextHolderFilter | 명시적 호출 필요 | FilterChainProxy |
FilterChainProxy는 Spring Security 사용의 핵심이기 때문에, 선택사항으로 보이지 않는 작업들을 수행할 수 있다. 예를 들어, 메모리 누수를 방지하기 위해 SecurityContext를 clear한다.
— Spring Security Architecture
// FilterChainProxy.java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
} finally {
securityContextHolderStrategy.clearContext(); // ← 여기서 clear!
request.removeAttribute(FILTER_APPLIED);
}
} else {
doFilterInternal(request, response, chain);
}
}
finally 블록이므로 예외가 발생하든 안 하든 무조건 실행된다!
컨텍스트가 저장된 ThreadLocal을 clearing하는 것은 필수적이다. 그렇지 않으면 스레드가 특정 사용자의 보안 컨텍스트가 여전히 붙어있는 상태로 서블릿 컨테이너의 스레드 풀에 반환될 수 있다. 이 스레드는 나중에 사용되어 잘못된 자격 증명으로 작업을 수행할 수 있다.
— Spring Security Core Web Filters
즉, 보안 사고 방지를 위해 반드시 clear 해야 한다!
SecurityContextHolder는 Strategy 패턴을 사용한다.
// SecurityContextHolder.java
public static void clearContext() {
strategy.clearContext(); // 전략에 위임!
}
// SecurityContextHolder.java
private static void initializeStrategy() {
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL"; // ← 기본값!
}
// strategyName에 따라 구현체 생성...
}
| 모드 | 구현체 | 설명 |
|---|---|---|
MODE_THREADLOCAL | ThreadLocalSecurityContextHolderStrategy | 기본값, 스레드별 저장 |
MODE_INHERITABLETHREADLOCAL | InheritableThreadLocalSecurityContextHolderStrategy | 자식 스레드에 상속 |
MODE_GLOBAL | GlobalSecurityContextHolderStrategy | 전역 저장 (서버 비권장) |
요청 시작
│
▼
FilterChainProxy.doFilter()
│
├── try {
│ │
│ ▼
│ SecurityContextHolderFilter
│ → SecurityContext 로드
│ │
│ ▼
│ JWT Filter
│ → 토큰 검증
│ → setAuthentication()
│ │
│ ▼
│ Controller 처리
│ }
│
└── finally {
securityContextHolderStrategy.clearContext()
│
▼
ThreadLocalSecurityContextHolderStrategy.clearContext()
│
▼
ThreadLocal.remove() ← 실제 제거!
}
│
▼
응답 반환
SecurityContextHolder는 기본적으로 ThreadLocal에 인증 정보를 저장한다.
JWT(Stateless) 환경에서는 세션을 사용하지 않으므로 매 요청마다 setAuthentication() 해야 한다.
요청이 끝나면 FilterChainProxy의 finally 블록에서 자동으로 clear된다.
Clear 하지 않으면 Thread Pool에서 스레드 재사용 시 이전 사용자 정보가 남아있을 수 있다 (보안 사고!)
SecurityContextHolder는 Strategy 패턴을 사용하며, 기본 전략은 MODE_THREADLOCAL이다.