SecurityContextHolder
1. 개요
1. 개요
SecurityContextHolder는 Spring Security 프레임워크의 핵심 컴포넌트이다. 이 객체는 현재 실행 중인 스레드에 대한 보안 컨텍스트 정보를 저장하고 제공하는 역할을 담당한다. 애플리케이션 코드 내 어디에서나 인증된 사용자의 정보와 권한을 조회할 수 있도록 하는 진입점 역할을 한다.
기본적으로 ThreadLocal을 사용하는 전략을 통해 동작한다. 이 방식은 각 스레드가 자신만의 독립적인 SecurityContext 인스턴스를 가지도록 보장한다. 따라서 한 스레드에서 설정된 보안 정보는 다른 스레드에 영향을 주지 않으며, 이는 특히 멀티스레드 환경인 웹 애플리케이션 서버에서 요청 간 보안 정보의 격리를 유지하는 데 필수적이다.
주요 용도는 인증이 완료된 사용자, 즉 Principal의 정보와 그에 부여된 권한(GrantedAuthority)을 애플리케이션 전역에서 편리하게 접근하는 것이다. 이를 통해 서비스 계층이나 컨트롤러에서 현재 사용자가 누구인지, 어떤 작업을 수행할 권한이 있는지를 쉽게 판단할 수 있다.
핵심적인 메서드로는 현재 컨텍스트를 가져오는 getContext(), 새로운 컨텍스트를 설정하는 setContext(), 그리고 컨텍스트를 초기화하는 clearContext()가 있다. 이 메서드들은 주로 인증 필터나 인터셉터에 의해 내부적으로 호출되며, 개발자는 주로 getContext()를 통해 저장된 정보를 조회하는 방식으로 사용하게 된다.
2. 역할과 중요성
2. 역할과 중요성
SecurityContextHolder는 스프링 시큐리티 애플리케이션에서 인증 정보를 관리하는 핵심적인 중앙 저장소 역할을 한다. 이 컴포넌트는 현재 실행 중인 스레드에 연결된 사용자의 보안 정보, 즉 SecurityContext를 보유하고 있으며, 애플리케이션 코드 내 어디에서나 이 정보에 접근할 수 있도록 하는 창구를 제공한다. 주로 SecurityContextHolder.getContext() 메서드를 통해 현재 스레드의 인증 객체를 얻어, 인증된 사용자(Principal)의 신원이나 권한(GrantedAuthority)을 확인하는 데 사용된다.
이 컴포넌트의 가장 중요한 역할은 인증 정보의 생명주기를 효율적으로 관리하는 것이다. 스프링 시큐리티의 필터 체인을 통해 사용자 요청이 인증되면, 그 결과로 생성된 Authentication 객체가 SecurityContextHolder에 설정된다. 이후 비즈니스 로직을 수행하는 서비스 계층이나 컨트롤러에서는 이 저장소를 조회하여 누가 요청을 보냈는지 알 필요 없이 일관된 방식으로 사용자 정보를 얻을 수 있다. 요청 처리가 완료되면, 해당 스레드가 재사용될 때 이전의 보안 정보가 남아 발생할 수 있는 보안 문제를 방지하기 위해 컨텍스트를 정리(clearContext())하는 것도 SecurityContextHolder의 책임이다.
SecurityContextHolder의 설계는 스레드 로컬을 기본 전략으로 채택함으로써 각 요청을 처리하는 스레드마다 완전히 독립적인 보안 컨텍스트를 유지할 수 있게 한다. 이는 멀티스레드 환경의 웹 애플리케이션에서 동시에 들어오는 여러 사용자의 요청이 서로의 보안 정보를 간섭하지 않고 안전하게 처리될 수 있도록 보장하는 기반이 된다. 따라서 이 컴포넌트는 스프링 시큐리티가 제공하는 선언적 보안 기능의 실제 동작을 뒷받침하는 실질적인 메커니즘이라고 할 수 있다.
3. 주요 구성 요소
3. 주요 구성 요소
3.1. SecurityContext
3.1. SecurityContext
SecurityContextHolder가 관리하는 핵심 객체는 SecurityContext이다. 이 객체는 현재 스레드와 연결된 보안 정보, 특히 인증 정보를 담는 컨테이너 역할을 한다. Spring Security 애플리케이션에서 사용자의 로그인 상태와 권한은 Authentication 객체로 표현되며, 이 객체가 바로 SecurityContext 내에 저장된다. 따라서 SecurityContextHolder.getContext() 메서드를 호출하여 SecurityContext를 얻고, 그 안에서 getAuthentication() 메서드로 현재 인증 정보를 조회하는 것이 일반적인 사용 패턴이다.
SecurityContext 인터페이스는 주로 인증 정보를 설정하고 조회하는 두 가지 주요 메서드를 제공한다. setAuthentication(Authentication authentication) 메서드는 새로운 인증 객체를 컨텍스트에 저장하는 데 사용된다. 이 메서드는 사용자 로그인 프로세스가 성공적으로 완료된 후, 생성된 Authentication 객체를 컨텍스트에 설정할 때 호출된다. 반대로 getAuthentication() 메서드는 현재 저장된 인증 정보를 반환하며, 인증된 사용자의 아이디나 권한 목록을 확인해야 하는 비즈니스 로직 전반에서 활용된다.
이 컨텍스트는 기본적으로 스레드 로컬 저장소에 보관되므로, 각 HTTP 요청을 처리하는 스레드는 자신만의 독립적인 SecurityContext를 가지게 된다. 이 설계 덕분에 다중 사용자 환경에서도 사용자별 보안 정보가 서로 간섭하지 않고 안전하게 관리될 수 있다. 요청 처리가 완료되면 SecurityContextHolder.clearContext() 메서드를 호출하여 해당 스레드의 컨텍스트를 정리하는 것이 좋다. 이는 메모리 누수를 방지하고, 스레드 풀에 반환된 스레드가 이전 사용자의 보안 정보를 유출하지 않도록 하는 데 도움이 된다.
3.2. 전략 (Strategy)
3.2. 전략 (Strategy)
SecurityContextHolder는 스프링 시큐리티 애플리케이션 내에서 현재 인증된 사용자의 정보를 담고 있는 SecurityContext를 저장하고 접근할 수 있도록 하는 중앙 저장소 역할을 한다. 이 저장소에 접근하는 방식을 결정하는 것이 바로 '전략(Strategy)'이다. SecurityContextHolder는 내부적으로 ThreadLocal을 사용하여 각 스레드마다 독립적인 SecurityContext 인스턴스를 보관하는 방식을 기본 전략으로 채택하고 있다. 이는 멀티스레드 환경에서 각 요청을 처리하는 스레드가 서로의 보안 정보를 간섭하지 않고 안전하게 관리될 수 있도록 보장한다.
SecurityContextHolder의 전략은 크게 세 가지 모드로 설정할 수 있으며, 이는 애플리케이션의 아키텍처나 특정 실행 환경에 맞게 선택된다. 가장 일반적으로 사용되는 모드는 MODE_THREADLOCAL로, 각 실행 스레드에 SecurityContext를 바인딩한다. MODE_INHERITABLETHREADLOCAL은 부모 스레드의 컨텍스트를 새로 생성된 자식 스레드에게 상속시키는 방식이며, MODE_GLOBAL은 애플리케이션 전체에서 단 하나의 SecurityContext 인스턴스를 모든 스레드가 공유하는 방식이다.
전략 모드는 주로 SecurityContextHolder의 정적 메서드 setStrategyName(String strategyName)을 호출하거나, spring.security.strategy 시스템 프로퍼티를 설정하는 방식으로 지정한다. 적절한 전략 선택은 서블릿 기반의 전형적인 웹 애플리케이션, 비동기 처리, 스케줄링 작업, 또는 RMI와 같은 분산 환경에서 SecurityContext 정보의 일관성과 안정성을 유지하는 데 중요하다.
4. 전략 모드
4. 전략 모드
4.1. MODE_THREADLOCAL
4.1. MODE_THREADLOCAL
MODE_THREADLOCAL은 SecurityContextHolder의 기본 전략으로 설정되는 모드이다. 이 모드는 자바의 ThreadLocal 변수를 활용하여, 현재 실행 중인 각 스레드마다 독립적인 SecurityContext를 저장하고 관리한다. 이 방식은 웹 애플리케이션에서 각 사용자의 HTTP 요청이 별도의 스레드에서 처리되는 일반적인 구조와 매우 잘 맞아떨어진다. 하나의 요청을 처리하는 동안에는 동일한 스레드가 사용되므로, 해당 스레드에 저장된 인증 정보를 애플리케이션 코드 어디서나 일관되게 조회할 수 있다.
이 전략의 가장 큰 장점은 스레드 간의 보안 정보가 완전히 분리된다는 점이다. 스레드 풀 환경에서도, 한 스레드가 처리하던 사용자의 SecurityContext가 다른 사용자의 요청을 처리하는 스레드에 섞여 전파되는 것을 방지한다. 이는 높은 수준의 보안과 데이터 무결성을 보장하며, 멀티스레딩 애플리케이션에서 발생할 수 있는 복잡한 동시성 문제를 근본적으로 차단한다.
MODE_THREADLOCAL은 명시적으로 다른 전략을 설정하지 않는 한 Spring Security가 자동으로 적용하는 방식이다. 개발자는 SecurityContextHolder의 정적 메서드인 getContext()를 호출하기만 하면, 현재 스레드에 바인딩된 Authentication 객체를 손쉽게 얻을 수 있다. 이 객체를 통해 인증된 사용자의 신원(Principal)과 부여된 권한(GrantedAuthority) 목록을 확인할 수 있다.
단, 이 모드는 순수한 ThreadLocal을 사용하기 때문에, 부모 스레드에서 생성된 자식 스레드로 보안 컨텍스트가 자동으로 상속되지 않는다는 점에 유의해야 한다. 비동기 처리나 별도의 스레드를 생성하는 작업을 수행할 때는 컨텍스트를 명시적으로 전파해 주어야 한다. 이러한 상속이 필요한 경우에는 MODE_INHERITABLETHREADLOCAL 전략을 고려할 수 있다.
4.2. MODE_INHERITABLETHREADLOCAL
4.2. MODE_INHERITABLETHREADLOCAL
MODE_INHERITABLETHREADLOCAL은 SecurityContextHolder의 전략 중 하나로, ThreadLocal을 기반으로 하면서도 자식 스레드가 부모 스레드의 보안 컨텍스트를 상속받을 수 있도록 합니다. 이는 스프링 시큐리티의 기본 전략인 MODE_THREADLOCAL과의 핵심적인 차이점입니다.
이 전략은 주로 새로운 스레드를 생성하여 비동기적으로 작업을 수행해야 하는 경우에 유용합니다. 예를 들어, @Async 어노테이션을 사용한 비동기 메서드 호출이나, 수동으로 ExecutorService를 통해 새로운 스레드를 생성하여 작업을 위임할 때, 부모 스레드에 설정된 인증 정보가 자식 스레드로 자동으로 전파되지 않으면 보안 문제가 발생할 수 있습니다. MODE_INHERITABLETHREADLOCAL 전략은 이러한 상황에서 부모 스레드의 SecurityContext를 자식 스레드가 물려받도록 보장하여, 애플리케이션 전반에서 일관된 보안 정보 접근을 가능하게 합니다.
이 전략을 활성화하기 위해서는 일반적으로 애플리케이션의 초기화 단계에서 SecurityContextHolder.setStrategyName 메서드를 호출하거나, 시스템 프로퍼티를 설정합니다. 설정 후에는 부모 스레드에서 SecurityContextHolder.getContext()를 통해 설정한 Authentication 객체가, 해당 부모 스레드로부터 생성된 자식 스레드 내에서도 동일하게 조회됩니다.
하지만 모든 스레드 생성 시 보안 컨텍스트를 복사해야 하므로 MODE_THREADLOCAL에 비해 약간의 성능 오버헤드가 발생할 수 있으며, 스레드 풀을 재사용하는 환경에서는 예상치 못한 컨텍스트 공유가 일어날 수 있어 주의가 필요합니다. 따라서 명시적으로 비동기 작업 간 보안 컨텍스트 공유가 필요한 시나리오에서 선택적으로 적용하는 것이 바람직합니다.
4.3. MODE_GLOBAL
4.3. MODE_GLOBAL
MODE_GLOBAL 전략은 Spring Security의 SecurityContextHolder가 사용하는 저장 방식 중 하나로, 애플리케이션 내 모든 스레드가 하나의 공유된 SecurityContext 인스턴스를 참조하도록 설정한다. 이 모드는 JVM 내에서 정적(static) 변수로 보안 컨텍스트를 관리하는 방식으로, 설정은 SecurityContextHolder.setStrategyName("MODE_GLOBAL") 메서드를 통해 이루어진다.
이 전략은 주로 독립적인 데스크톱 애플리케이션이나 스윙 기반의 클라이언트 애플리케이션에서 사용되도록 설계되었다. 이러한 환경에서는 사용자 인터페이스 이벤트를 처리하는 모든 스레드가 단일 사용자에 대응되는 경우가 일반적이기 때문이다. 반면, 서블릿 컨테이너 기반의 대표적인 웹 애플리케이션 서버 환경에서는 각 HTTP 요청이 별도의 스레드에서 처리되고 여러 사용자의 요청이 동시에 들어오므로, MODE_GLOBAL을 사용하면 모든 사용자의 인증 정보가 하나의 컨텍스트에 섞이는 심각한 보안 문제가 발생한다.
따라서 표준 스프링 부트 웹 애플리케이션에서는 이 방식을 사용하지 않는다. 기본값이자 가장 널리 쓰이는 전략은 각 스레드의 독립된 저장 공간인 ThreadLocal을 사용하는 MODE_THREADLOCAL이다. MODE_GLOBAL은 특정 클라이언트 애플리케이션 아키텍처에서만 제한적으로 고려되어야 하며, 대부분의 개발자는 이 모드 대신 MODE_THREADLOCAL이나 자식 스레드에 컨텍스트를 상속하는 MODE_INHERITABLETHREADLOCAL을 사용하게 된다.
5. 사용 방법
5. 사용 방법
SecurityContextHolder를 사용하는 기본적인 방법은 현재 스레드의 보안 컨텍스트를 가져오고 설정하는 것이다. 가장 일반적인 사용 사례는 컨트롤러나 서비스 계층에서 현재 인증된 사용자의 정보를 조회하는 것이다. SecurityContextHolder.getContext() 메서드를 호출하여 SecurityContext 객체를 얻고, 여기서 Authentication 객체를 추출하여 사용자명이나 권한을 확인할 수 있다.
구체적인 사용 예시로, 인증된 사용자의 이름을 가져오려면 SecurityContextHolder.getContext().getAuthentication().getName()을 호출한다. 사용자가 가진 권한 목록을 확인하려면 getAuthorities() 메서드를 사용한다. 또한, 특정 스레드에 보안 정보를 수동으로 설정해야 하는 경우, 예를 들어 배치 작업이나 비동기 작업을 시작할 때, SecurityContextHolder.setContext() 메서드를 사용하여 미리 구성된 SecurityContext를 해당 스레드에 할당할 수 있다.
사용이 끝난 후에는 보안 컨텍스트를 정리하는 것이 중요하다. 특히 ThreadLocal 전략을 사용할 때는 스레드 풀 환경에서 스레드가 재사용될 경우 이전 사용자의 보안 정보가 남아 있을 수 있어 보안 문제가 발생할 수 있다. 따라서 요청 처리가 완료되면 SecurityContextHolder.clearContext() 메서드를 호출하여 현재 스레드의 컨텍스트를 명시적으로 제거해야 한다.
SecurityContextHolder는 주로 Spring Security의 필터 체인에 의해 자동으로 관리되므로, 애플리케이션 코드에서 직접 setContext()를 호출하는 경우는 제한적이다. 대부분의 경우 @AuthenticationPrincipal 어노테이션을 사용하거나 SecurityContextHolder.getContext()를 통해 정보를 조회하는 방식으로 사용하게 된다.
6. 주의사항
6. 주의사항
SecurityContextHolder를 사용할 때는 몇 가지 중요한 주의점이 있다. 기본적으로 스레드 로컬을 사용하기 때문에, 비동기 작업이나 별도의 스레드를 생성하는 경우 컨텍스트 정보가 제대로 전파되지 않을 수 있다. 예를 들어, @Async 어노테이션을 사용한 비동기 메서드 내부나 새로운 스레드를 직접 생성하여 실행하는 경우, 부모 스레드의 SecurityContext가 자동으로 상속되지 않는다. 이로 인해 인증 정보가 null이 되어 NullPointerException이 발생하거나 권한 검사에 실패할 수 있다.
이러한 문제를 해결하기 위해 스프링 시큐리티는 MODE_INHERITABLETHREADLOCAL 전략이나 DelegatingSecurityContextRunnable, DelegatingSecurityContextExecutor와 같은 헬퍼 클래스를 제공한다. 또한, 웹 애플리케이션에서는 요청 처리가 완료된 후 스레드가 스레드 풀로 반환되기 전에 SecurityContextHolder.clearContext()를 호출하여 컨텍스트를 정리해야 한다. 이를 통해 이전 사용자의 인증 정보가 다음 요청에 유출되는 것을 방지할 수 있다.
테스트 환경에서도 주의가 필요하다. 단위 테스트 시 SecurityContextHolder에 직접 인증 객체를 설정한 경우, 테스트 실행 후 컨텍스트를 명시적으로 초기화하지 않으면 다른 테스트 케이스에 영향을 미칠 수 있다. 따라서 각 테스트 메서드의 @AfterEach 또는 @AfterAll 단계에서 clearContext()를 호출하는 것이 좋다. SecurityContextHolder는 편리한 전역 접근 방식을 제공하지만, 이러한 특성 때문에 애플리케이션 설계 시 의존성을 과도하게 증가시킬 수 있으므로 신중하게 사용해야 한다.
