💡 오늘 학습 키워드
- Authentication
- SecurityContextHolder
🎯 학습 내용 정리
Spring Security를 처음 학습하면 Authentication, SecurityContext, SecurityContextHolder라는 객체를 자주 접하게 된다. JWT 인증을 구현하면서도 다음과 같은 코드를 한 번쯤 작성하게 된다.
SecurityContextHolder.getContext()
.setAuthentication(authentication);
이 코드 한 줄만으로 Spring Security는 현재 요청을 인증된 사용자로 인식하고, Controller에서는 @AuthenticationPrincipal을 통해 로그인한 사용자의 정보를 바로 사용할 수 있다.
하지만 많은 개발자는 Authentication을 어디에 저장하는지, SecurityContext는 어떤 역할을 하는지, ThreadLocal은 왜 사용하는지를 명확하게 이해하지 못한 채 구현하는 경우가 많다.
Spring Security가 인증 정보를 저장하고 관리하는 내부 구조를 살펴보고, 요청이 종료될 때까지 인증 정보가 유지되는 원리를 알아보자.
Spring Security의 핵심 객체
JWT 인증을 수행하면 가장 중요한 네 가지 객체가 등장한다.
- Authentication
- SecurityContext
- SecurityContextHolder
- ThreadLocal
이들은 각각 서로 다른 역할을 담당한다.
| 객체 | 역할 |
| Authentication | 로그인 사용자 정보 |
| SecurityContext | Authentication 저장소 |
| SecurityContextHolder | SecurityContext 접근 객체 |
| ThreadLocal | 현재 요청(Thread)마다 SecurityContext 저장 |
Authentication은 사용자 정보를 담고, SecurityContext는 Authentication을 보관하며, SecurityContextHolder는 SecurityContext에 접근하기 위한 진입점 역할을 수행한다.
1. Authentication
Authentication은 Spring Security에서 현재 인증된 사용자를 표현하는 객체이다.
JWT 자체를 사용하는 것이 아니라 JWT에서 사용자 정보를 추출하여 Authentication으로 변환한다.
가장 많이 사용하는 구현체는 UsernamePasswordAuthenticationToken이다.
Authentication authentication =
new UsernamePasswordAuthenticationToken(
principal,
null,
authorities
);
Authentication 내부에는 다음과 같은 정보가 저장된다.
Principal
Credentials
Authorities
Authenticated
각 항목의 의미는 다음과 같다.
| 필드 | 설명 |
| Principal | 로그인 사용자 |
| Credentials | 비밀번호 또는 인증 정보 |
| Authorities | 권한 목록 |
| Authenticated | 인증 여부 |
JWT 인증에서는 이미 로그인이 완료된 이후이므로 Credentials는 일반적으로 null로 설정한다.
Principal에는 무엇이 저장될까?
Principal에는 현재 로그인한 사용자를 표현하는 객체가 저장된다.
일반적으로 UserDetails를 구현한 객체를 사용한다.
public class CustomUserDetails implements UserDetails {
private final Long userId;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(
Long userId,
String username,
Collection<? extends GrantedAuthority> authorities
) {
this.userId = userId;
this.username = username;
this.authorities = authorities;
}
public Long getUserId() {
return userId;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getUsername() {
return username;
}
// 생략
}
이 객체가 Controller에서 @AuthenticationPrincipal로 주입된다.
2. SecurityContext
Authentication이 생성되었다고 해서 Spring Security가 자동으로 인증 상태를 유지하는 것은 아니다.
Authentication을 저장할 공간이 필요하다.
바로 SecurityContext가 그 역할을 수행한다.
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContext는 Authentication 하나만을 관리하는 매우 단순한 객체이다.
구조는 다음과 같다.
SecurityContext
↓
Authentication
SecurityContext를 직접 생성하고 전달하는 것은 매우 번거로운 작업이다.
Spring Security는 어디서든 현재 SecurityContext에 접근할 수 있도록 SecurityContextHolder를 제공한다.
Authentication을 저장하는 과정은 다음과 같다.
SecurityContextHolder.getContext()
.setAuthentication(authentication);
Authentication을 조회하는 방법도 동일하다.
Authentication authentication =
SecurityContextHolder.getContext()
.getAuthentication();
SecurityContextHolder는 내부적으로 ThreadLocal을 사용하여 현재 요청의 SecurityContext를 관리한다.
ThreadLocal은 왜 사용할까?
웹 서버에서는 동시에 수많은 요청이 처리된다.
예를 들어 두 명의 사용자가 동시에 요청을 보내는 상황을 생각해보자.
User A
↓
Thread 1
----------------
User B
↓
Thread 2
만약 Authentication을 전역 변수에 저장한다면 서로의 인증 정보가 덮어쓰이게 된다.
이를 방지하기 위해 Spring Security는 ThreadLocal을 사용한다.
ThreadLocal은 Thread마다 독립적인 데이터를 저장하는 자료구조이다.
즉, 각 요청은 자신의 Thread에서만 Authentication을 조회할 수 있다.
Controller에서는 어떻게 로그인 사용자를 사용할까?
Controller에서는 다음과 같이 작성할 수 있다.
@GetMapping("/me")
public UserResponse getMyInfo(
@AuthenticationPrincipal CustomUserDetails user
) {
return userService.findById(user.getUserId());
}
Spring Security는 내부적으로 다음 과정을 수행한다.
SecurityContextHolder
↓
SecurityContext
↓
Authentication
↓
Principal
↓
@AuthenticationPrincipal
개발자가 직접 SecurityContext를 조회하지 않아도 Spring Security가 Principal을 자동으로 주입한다.
요청이 종료되면 Authentication은 어떻게 될까?
Authentication은 영구적으로 저장되지 않는다. 요청이 종료되면 SecurityContext도 함께 제거된다.
Spring Security는 요청 종료 시 다음과 같은 작업을 수행한다.
SecurityContextHolder.clearContext();
이 과정이 없다면 다음 요청에서 이전 사용자의 Authentication이 남아 심각한 보안 문제가 발생할 수 있다.
지금까지의 내용을 하나의 흐름으로 정리하면 다음과 같다.
JWT
↓
Authentication 생성
↓
SecurityContext 저장
↓
SecurityContextHolder
↓
ThreadLocal
↓
AuthorizationFilter
↓
Controller
↓
@AuthenticationPrincipal
↓
Response
↓
clearContext()
실무에서 자주 하는 실수에 대해서 알아보자.
1. Authentication을 생성만 하고 저장하지 않는 경우
Authentication authentication =
jwtProvider.getAuthentication(token);
위 코드만 실행하면 인증이 완료되지 않는다.
반드시 SecurityContext에 저장해야 한다.
SecurityContextHolder.getContext()
.setAuthentication(authentication);
2. SecurityContextHolder를 직접 사용하는 경우
Service나 Repository에서 SecurityContextHolder를 직접 호출하면 Security 계층과 비즈니스 계층이 강하게 결합된다.
예를 들어 다음과 같은 코드는 가능하면 지양하는 것이 좋다.
@Service
public class UserService {
public UserResponse getCurrentUser() {
Authentication authentication =
SecurityContextHolder.getContext()
.getAuthentication();
CustomUserDetails principal =
(CustomUserDetails) authentication.getPrincipal();
return findById(principal.getUserId());
}
}
대신 Controller에서 @AuthenticationPrincipal 또는 인증 사용자 ID를 전달받아 Service가 보안 구현에 의존하지 않도록 설계하는 것이 유지보수성과 테스트 용이성 측면에서 유리하다.
@GetMapping("/me")
public UserResponse getMyInfo(
@AuthenticationPrincipal CustomUserDetails user
) {
return userService.findById(user.getUserId());
}
@Service
public class UserService {
public UserResponse findById(Long userId) {
// 비즈니스 로직
}
}
3. ThreadLocal의 특성을 고려하지 않는 경우
SecurityContextHolder는 기본적으로 ThreadLocal을 사용하므로 동일한 요청을 처리하는 Thread에서만 인증 정보를 조회할 수 있다.
따라서 @Async, 별도 스레드 생성, 비동기 작업에서는 현재 요청의 Authentication이 자동으로 전달되지 않는다.
이 점을 이해하지 못하면 비동기 로직에서 인증 정보가 null이 되는 문제를 자주 경험하게 된다.
Spring Security는 JWT 자체를 인증 객체로 사용하는 것이 아니라, JWT의 Claims를 기반으로 Authentication을 생성하고 이를 SecurityContext에 저장하여 인증 상태를 관리한다. SecurityContextHolder는 현재 요청의 SecurityContext에 접근하는 진입점이며, 내부적으로 ThreadLocal을 사용하여 요청마다 독립적인 인증 정보를 유지한다.
Controller에서 @AuthenticationPrincipal이 동작하는 이유도 결국 SecurityContextHolder에 저장된 Authentication의 Principal을 Spring Security가 자동으로 주입하기 때문이다. 또한 요청이 종료되면 SecurityContextHolder.clearContext()를 호출하여 인증 정보를 제거함으로써 다음 요청에 인증 정보가 남지 않도록 보장한다.
이 구조를 이해하면 Authentication이 null이 되는 문제, 권한이 정상적으로 인식되지 않는 문제, 비동기 환경에서 인증 정보가 전달되지 않는 문제 등 Spring Security에서 자주 발생하는 이슈의 원인을 보다 정확하게 분석할 수 있다.
📚 한줄 정리
Spring Security의 인증은 Authentication을 SecurityContextHolder에 저장하고, 요청이 끝날 때까지 이를 안전하게 관리하는 과정이라고 이해하면 전체 동작 원리를 훨씬 쉽게 파악할 수 있다.
'Bootcamp > Implementation' 카테고리의 다른 글
| AuthenticationManager와 AuthenticationProvider (0) | 2026.07.10 |
|---|---|
| UserDetails와 UserDetailsService (0) | 2026.07.09 |
| JWT 기반 로그인 구현과 Access Token 발급 (0) | 2026.07.07 |
| Spring Security 구조 설계 (0) | 2026.07.06 |
| OAuth (0) | 2026.06.28 |