본문 바로가기
language/java

ThreadLocal과 Spring Security

by 죄니안죄니 2026. 6. 24.
반응형

여기서 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

댓글