여기서 ThreadLocal과 Spring Security를 넣는 이유는 명확하다.
방금 전 글에서는
ThreadLocal에는 Connection이 저장된다.
를 배웠다.
이번 글에서는
Connection뿐만 아니라 로그인 사용자 정보도 ThreadLocal에 저장된다.
를 보여주면 독자는
"아, Spring은 요청(Request)마다 필요한 상태를 ThreadLocal에 저장하는구나."
를 자연스럽게 이해하게 된다.
ThreadLocal과 Spring Security
Spring은 로그인한 사용자를 어디에 보관할까?
이전 글에서는
@Transactional이 내부적으로
Controller
↓
Transaction Proxy
↓
ThreadLocal(Connection 저장)
↓
Repository
↓
ThreadLocal(Connection 조회)
↓
Commit
↓
ThreadLocal.remove()
구조로 동작한다는 것을 배웠다.
그런데 이번에는 다른 질문을 해보자.
Controller에서 우리는 종종 이런 코드를 작성한다.
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
또는
@AuthenticationPrincipal
CustomUser user
를 사용한다.
여기서 질문이 생긴다.
로그인한 사용자는 어디에 저장되어 있을까?
매번 Database를 조회하는 걸까?
답은
ThreadLocal이다.
로그인은 한 번만 수행된다.
사용자가 로그인한다.
ID / Password 입력
↓
인증 성공
↓
Authentication 생성
Spring Security는
Authentication 객체를 만든다.
Authentication authentication =
new UsernamePasswordAuthenticationToken(
user,
null,
authorities
);
이 객체에는
사용자 정보
권한(Role)
인증 상태
가 들어 있다.
그런데 어디에 저장할까?
Spring Security는
SecurityContext를 만든다.
SecurityContext
↓
Authentication
그리고
이 SecurityContext를
ThreadLocal에 저장한다.
그림으로 보면
Thread-1
↓
ThreadLocalMap
↓
SecurityContext
↓
Authentication
↓
CustomUser
즉,
현재 요청(Request)을 처리하는 Thread는
항상 자신의 로그인 정보를 가지고 있다.
Controller에서는
@GetMapping("/me")
public UserResponse me() {
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
}
겉보기에는
단순한 메서드 호출처럼 보인다.
실제로는
현재 Thread
↓
ThreadLocalMap
↓
SecurityContext
↓
Authentication 반환
이다.
SecurityContextHolder 내부 구조
실제로는 매우 단순하다.
public class SecurityContextHolder {
private static SecurityContextHolderStrategy strategy;
}
기본 Strategy는
ThreadLocalSecurityContextHolderStrategy
이다.
그리고 내부에는
private static final ThreadLocal<SecurityContext>
contextHolder = new ThreadLocal<>();
가 존재한다.
즉,
SecurityContextHolder.getContext();
는 사실상
threadLocal.get();
를 호출하는 것과 비슷하다.
흐름으로 보면
로그인
Login
↓
Authentication 생성
↓
SecurityContext 생성
↓
ThreadLocal 저장
API 호출
Controller
↓
SecurityContextHolder
↓
ThreadLocal 조회
↓
Authentication 반환
응답 종료
Request 종료
↓
SecurityContextHolder.clearContext()
↓
ThreadLocal.remove()
왜 ThreadLocal을 사용할까?
매번
userService.findUser(...)
를 호출한다면
Controller
↓
Service
↓
Repository
↓
DB 조회
매 요청마다
로그인 정보를 다시 읽어야 한다.
ThreadLocal을 사용하면
Request 시작
↓
한 번 저장
↓
Controller
↓
Service
↓
Repository
↓
언제든 조회 가능
이다.
Service에서도 사용 가능
Controller뿐만 아니라
Service에서도
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
를 사용할 수 있다.
왜냐하면
Controller와 Service는
같은 Thread에서 실행되기 때문이다.
그림으로 이해
Request
↓
Thread-1
↓
Controller
↓
Service
↓
Repository
↓
Response
Thread가 하나이므로
ThreadLocal도 하나이다.
@AuthenticationPrincipal은 어떻게 동작할까?
Controller
@GetMapping("/me")
public UserResponse me(
@AuthenticationPrincipal
CustomUser user
) {
}
Spring MVC는
내부적으로
ThreadLocal
↓
SecurityContext
↓
Authentication
↓
Principal
↓
Controller 파라미터 주입
을 수행한다.
개발자는
ThreadLocal을 직접 사용할 필요가 없다.
ThreadLocal.remove()가 중요한 이유
Tomcat은
ThreadPool을 사용한다.
Request1
↓
Thread-1
↓
SecurityContext 저장
↓
Request 종료
remove()를 하지 않으면
Thread-1
↓
이전 사용자 정보 유지
다음 요청
Request2
↓
Thread-1 재사용
↓
이전 로그인 사용자 조회
엄청난 보안 문제가 발생한다.
그래서 Spring Security는
항상
try {
filterChain.doFilter(...);
}
finally {
SecurityContextHolder.clearContext();
}
를 수행한다.
Transaction과 비교
Transaction
ThreadLocal
↓
Connection
Security
ThreadLocal
↓
Authentication
구조는 완전히 동일하다.
이전 글들과 연결
지금까지 배운 내용을 연결하면
HTTP Request
│
▼
Security Filter
│
▼
ThreadLocal(Authentication 저장)
│
▼
Controller
│
▼
Transaction Proxy
│
▼
ThreadLocal(Connection 저장)
│
▼
Service
│
▼
Repository
│
▼
DB
│
▼
Commit
│
▼
ThreadLocal(Connection 제거)
│
▼
Response
│
▼
ThreadLocal(Authentication 제거)
ThreadLocal이 Spring에서 사용되는 대표 사례
ThreadLocal
├── Transaction
│ └── Connection
│
├── Spring Security
│ └── Authentication
│
├── RequestContextHolder
│ └── HttpServletRequest
│
├── LocaleContextHolder
│ └── Locale
│
└── MDC(Logback)
└── TraceId
실무에서 흔히 하는 실수
직접
private static final ThreadLocal<User>
holder = new ThreadLocal<>();
를 만들고
holder.set(user);
만 하고
remove를 하지 않는 경우이다.
Spring Security는
finally {
SecurityContextHolder.clearContext();
}
를 통해 항상 정리한다.
직접 ThreadLocal을 사용할 때도 반드시 같은 패턴을 따라야 한다.
면접 포인트
Spring Security는 로그인 사용자를 어디에 저장하는가?
기본적으로 SecurityContextHolder 내부의 ThreadLocal<SecurityContext>에 저장한다.
Controller와 Service에서 모두 로그인 정보를 조회할 수 있는 이유는?
하나의 HTTP 요청은 일반적으로 하나의 Thread에서 처리되며, Controller와 Service가 동일한 ThreadLocal을 공유하기 때문이다.
왜 SecurityContextHolder.clearContext()가 필요한가?
Tomcat은 ThreadPool을 사용하므로 Thread가 재사용된다. ThreadLocal을 정리하지 않으면 이전 요청의 사용자 정보가 다음 요청에 남아 심각한 보안 문제가 발생할 수 있다.
다음 글 추천
다음 글은 ThreadLocal과 TransactionSynchronizationManager를 추천한다.
이 글에서는
ThreadLocal
↓
TransactionSynchronizationManager
↓
Connection
SqlSession
Hibernate Session
Transaction 상태
Resource Binding
까지 설명하면,
독자는 "Spring은 ThreadLocal을 이용해 요청 단위 상태를 관리하는 프레임워크"라는 관점을 갖게 되고 Spring 내부 구조를 훨씬 깊이 이해할 수 있게 된다.
'language > java' 카테고리의 다른 글
| Annotation 기반 동작 원리 (0) | 2026.06.24 |
|---|---|
| ExecutorService와 @Async (0) | 2026.06.24 |
| ThreadLocal과 TransactionSynchronizationManager (0) | 2026.06.24 |
| Java Proxy 기반 트랜잭션 처리 (0) | 2026.06.17 |
| JDK Proxy vs CGLIB in Spring (0) | 2026.06.17 |
댓글