개발자 유미의 시큐리티 강의를 보고 작성했습니다.
SecurityContextHolder

Spring Security의 SecurityFilterChain은 인증과 인가 같은 보안 기능을 여러 필터에 나누어 수행합니다. 각 필터는 독립적으로 동작하지만, 앞에서 처리한 정보를 다음 필터가 활용해야 하는 경우가 자주 발생합니다. 이를 위해 필터 간에 상태를 공유할 수 있는 구조가 필요합니다.
Spring Security는 이러한 목적을 위해 SecurityContextHolder라는 저장소 개념을 제공합니다. 예를 들어, 인가를 담당하는 필터는 현재 사용자의 권한 정보를 확인해야 합니다. 이 권한 정보는 앞 단계의 인증 필터에서 생성된 Authentication 객체에 담겨 있으며, 해당 객체는 SecurityContext에 저장됩니다.
Authentication 객체에는 사용자 정보, 인증 방식, 권한 목록 등이 포함되어 있고, 이 SecurityContext는 SecurityContextHolder를 통해 애플리케이션 전역에서 접근할 수 있습니다. 이렇게 함으로써 하나의 요청 흐름 내에서 인증 정보를 여러 필터나 서비스 로직에서 일관되게 사용할 수 있게 됩니다.
시큐리티 유저 정보 저장 : Authentication 객체

Authentication 객체는 크게 세 가지 구성 요소로 나뉜다.
| Principal은 사용자 고유의 식별자나 계정 정보를 담는다 |
| Credentials는 비밀번호나 인증 토큰과 같은 사용자의 인증 수단이다 |
| Authorities는 사용자가 가진 권한 목록으로, 이 정보가 인가 로직에서 핵심적인 역할을 한다 |
SecurityContextHolder.getContext().getAuthentication().getAuthorities();
Spring Security에서 인가 과정은 SecurityContextHolder에 저장된 인증 정보를 활용해 사용자의 권한을 검사하는 방식으로 동작합니다. 실제로 인가 처리 시에는 위의 코드로 현재 사용자의 권한 정보를 조회하고, 이를 바탕으로 접근을 허용하거나 차단합니다. 이러한 구조 덕분에 SecurityContextHolder는 정적 메서드를 통해 언제 어디서든 접근할 수 있어 매우 편리하게 사용됩니다.
또한 SecurityContextHolder는 단일한 객체이지만, 내부적으로는 사용자마다 서로 다른 SecurityContext를 유지할 수 있도록 설계되어 있습니다. 즉, 여러 사용자가 동시에 서비스를 이용하더라도 각 사용자에 대해 독립적인 SecurityContext가 생성되고 관리됩니다.
각 SecurityContext는 해당 사용자의 인증 정보를 담고 있는 Authentication 객체를 보관하고 있으며, 이를 통해 로그인 상태, 사용자 정보, 권한 목록 등을 참조할 수 있습니다.
SecurityContextHolder 동작 방식
웹 애플리케이션은 여러 사용자의 요청을 동시에 처리하는 멀티스레드 환경에서 실행됩니다. 이러한 구조에서는 각 사용자에 대한 인증 정보가 스레드 간에 절대로 공유되어서는 안 되며, 사용자마다 격리된 보안 컨텍스트가 유지되어야 합니다.
이를 위해 Spring Security는 SecurityContextHolder 내부에 전략 객체를 두고, 각 요청 스레드가 독립적인 인증 정보를 가지도록 관리합니다. 이 전략 객체는 SecurityContextHolderStrategy라는 인터페이스를 기반으로 구현되며, 실제 인증 정보를 저장하고 조회하는 기능을 위임받습니다.
Spring Security는 기본적으로 ThreadLocal 기반 전략인 ThreadLocalSecurityContextHolderStrategy를 사용합니다. 이 전략은 다음과 같이 정의되어 있습니다
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal<>();
}
ThreadLocal은 같은 정적 필드를 사용하더라도 스레드마다 서로 다른 값을 유지할 수 있는 저장소입니다. 예를 들어, Tomcat 같은 WAS는 각 요청에 대해 별도의 스레드를 할당하는데, 이때 ThreadLocal을 활용하면 A 사용자의 인증 정보가 B 사용자와 절대 섞이지 않습니다.
Spring Security는 이 전략을 기본값으로 사용하지만, 필요에 따라 InheritableThreadLocal, 글로벌 공유 전략, 혹은 사용자 정의 전략 등으로 확장하여 사용할 수도 있습니다.
즉, SecurityContextHolder는 인증 정보를 직접 관리하지 않고, 내부 전략 객체에 위임함으로써 멀티스레드 환경에서도 안전하고 유연하게 인증 상태를 유지할 수 있도록 도와주는 구조입니다.

위 그림은 ThreadLocal의 동작 방식을 간단히 시각화한 것입니다. 일반적인 클래스 필드는 모든 스레드가 동일한 값을 참조하게 되지만, ThreadLocal을 사용하면 각 스레드마다 별도의 저장 공간을 가지게 됩니다.
그림에서처럼 하나의 ThreadLocal 필드는 클래스 수준에서는 단 하나만 존재하지만, 각 스레드는 자신만의 고유한 값을 이 필드를 통해 저장하고 조회합니다. 이 덕분에 여러 사용자가 동시에 요청을 보내더라도, 인증 정보가 서로 섞이지 않고 안전하게 격리됩니다.
Spring Security는 이 구조를 기반으로 SecurityContext를 저장합니다. 즉, 사용자가 요청을 보낼 때 해당 스레드에 Authentication 정보를 저장하고, 이후 필터나 서비스에서 SecurityContextHolder를 통해 꺼내서 활용할 수 있는 것입니다. 이렇게 함으로써 멀티스레드 환경에서도 보안 정보를 일관되게 관리할 수 있게 되는 것입니다.
필터의 상속

먼저, 가장 상단에는 서블릿 필터 인터페이스가 위치합니다. 이 인터페이스는 필터로서의 기본 계약을 정의하며, 그 아래에는 이를 구현한 추상 클래스 GenericFilterBean과 OncePerRequestFilter가 있습니다.
GenericFilterBean은 Spring에서 제공하는 공통 필터 기능을 포함하고 있고, OncePerRequestFilter는 HTTP 요청당 한 번만 필터가 실행되도록 보장하는 역할을 합니다.
하단의 "구현1", "구현2"로 표시된 박스들은 실질적으로 개발자가 작성하는 필터 구현 클래스들입니다. 이 클래스들은 각자 특정 필터링 목적(예: 인증, 인가, JWT 처리 등)을 담당하며, 위의 추상 클래스를 상속하여 필요한 로직만 구현합니다.
예를 들어 로그인 필터를 만든다고 할 때, 로그인에 대한 공통된 처리 구조는 구현1에서 작성하고, 그 안에서 실제 로그인 방식(폼 로그인, 소셜 로그인, JWT 로그인 등)은 구현2에서 나눠 구현하여 등록하는 식입니다.
이러한 구조의 장점은 다음과 같습니다. 먼저 중복되는 코드를 줄일 수 있습니다. 각 필터 구현체는 자신이 맡은 역할에만 집중하며, 상위 추상 클래스는 필터로서의 기본적인 구조나 부가 기능(log 처리, 예외 핸들링 등)을 제공합니다. 또한, 확장성이 높아지므로 새로운 로그인 방식이 추가될 경우에도 기존 코드를 수정하지 않고 새로운 구현2 클래스를 추가해 등록하면 됩니다.
GenericFilterBean과 OncePerRequestFilter
GenericFilterBean
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {
}
OncePerRequestFilter
public abstract class OncePerRequestFilter extends GenericFilterBean {
protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException;
}
GenericFilterBean은 자바 서블릿의 Filter 인터페이스를 구현한 Spring의 추상 클래스입니다. 이 클래스는 서블릿 환경에서 Spring 컨텍스트에 접근할 수 있도록 도와주며, 순수 자바 서블릿 필터 구조에 Spring의 기능을 통합한 기본 형태라고 볼 수 있습니다.
GenericFilterBean을 기반으로 작성된 필터는, 클라이언트의 한 번의 요청에 대해 필터가 여러 번 호출될 경우, 그 횟수만큼 내부 로직이 반복적으로 실행됩니다. 즉, 동일한 요청 흐름 안에서 여러 서블릿 컴포넌트를 거치며 해당 필터를 다시 통과하면 내부 처리 로직이 중복 실행될 수 있습니다.
반면, OncePerRequestFilter는 GenericFilterBean을 상속한 추상 클래스로, 이름 그대로 "요청당 한 번만 실행"되도록 보장하기 위해 설계되었습니다. 이 필터는 동일한 요청 흐름 내에서 여러 번 호출되더라도 내부 로직은 단 한 번만 수행되도록 제한합니다. 따라서 Spring Security에서는 인증처럼 중복 실행되면 안 되는 중요한 작업에 주로 사용됩니다.
forward 방식

여기서 말하는 “한 번의 요청”이란 개념은 forward와 redirect 방식에 따라 다르게 해석될 수 있습니다. forward는 서버 내부에서 경로만 전환되는 방식으로, 클라이언트 입장에서는 여전히 하나의 요청으로 간주됩니다. 따라서 이 경우 OncePerRequestFilter는 실제로 단 한 번만 실행됩니다.
redirect 방식

반면 redirect는 서버가 클라이언트에게 302 응답을 보내고, 클라이언트가 지정된 URL로 다시 요청을 보내는 방식입니다. 이때는 두 개의 완전한 HTTP 요청이 발생하게 되며, 그에 따라 OncePerRequestFilter도 각 요청마다 한 번씩, 총 두 번 실행됩니다.
이러한 차이점을 고려하면, 필터를 구현할 때도 전략적인 선택이 필요합니다. 요청 중 여러 번 실행되어도 문제가 없는 필터이거나, 오히려 매번 실행되어야 하는 경우에는 GenericFilterBean을 사용하는 것이 적절합니다.
반대로 인증 처리처럼 요청당 한 번만 실행되어야 하는 필터의 경우에는 OncePerRequestFilter를 상속받는 것이 바람직합니다. 이러한 구조는 필터의 역할을 명확히 분리하고, 불필요한 중복 실행을 방지하는 데에 큰 도움이 됩니다.
'CS' 카테고리의 다른 글
| 캐싱 전략 (0) | 2025.09.27 |
|---|---|
| 스프링 시큐리티 - 필터의 종류 (1) (0) | 2025.07.03 |
| 스프링 시큐리티 - 전체적인 흐름 (0) | 2025.06.28 |
| Redis가 싱글 스레드로 만들어진 이유 (2) | 2025.06.05 |
| MySQL InnoDB에서 갭락과 넥스트키 락이란 무엇이며, 어떻게 팬텀 리드를 방지하나요? (0) | 2025.05.29 |