카테고리 없음

[Spring Security] 비동기 환경에서의 인증 정보 전파와 트랜잭션 관리

땍땍 2026. 1. 11. 13:23

1. 기존 아키텍처 및 프로세스

현재 서버는 메인 서버와 수집 서버로 분리 되어, 메인 서버에서 특정 유저에 대한 정보가 필요할 때, 수집 서버에서 필요한 정보를 조합해 메인 서버로 요청을 보내는 구조를 가지고 있다. 이때 요청 방식은 크게 HTTP 요청과 메세지 큐(Message Queue)를 통한 발행 두 가지로 뉘었고, 식별 및 로깅 처리를 위해 메인 서버와 수집 서버 모두 어떤 사용자에 대한 요청인지 요청에 포함된 인증 정보를 파악하고 처리해야만 했다.

 

2. 문제점

기존에 HTTP 요청만 존재했을 때는 Filter 단에서 인증 정보를 추출하여 검증하면 간단히 해결되었다. 하지만 메세지 큐를 통해 들어오는 요청은 Filter를 거치지 않기 때문에, 동일한 로직으로는 유저 정보를 추출할 수 없는 문제가 발생했다.

 

[ Filter? Interceptor?, AOP? ]

인증 및 인가 처리는 어플리케이션 전반에 걸쳐 공통적으로 적용되어야만 하는 과정이기에 보통 서비스 로직 내에서 관리하는 것이 아닌 요청을 일괄적으로 처리할 수 있는 Filter, Interceptor, AOP 에서 보통 관리하곤 한다.

하지만 Filter 와 Interceptor 은 HTTP 요청에 특화되어 있어, 메세지 리스너가 소비하는 이벤트성 메세지를 감지하기가 어렵다는 문제가 존재하였고, 관리 포인트를 하나로 통일하기 위해 AOP에서 모든 인증 정보를 일괄 관리하는 방안도 고려했으나, HTTP 요청의 경우 앞단(Filter, Interceptor)에서 헤더 정보가 조작될 수 있다는 문제점이 존재하였기에 결과적으로 HTTP 요청은 Filter에서, 메세지 요청은 AOP에서 각각 인증 정보를 추출하여 관리하기로 결정했다.

  • HTTP: Header에 정보 포함
  • Message: Payload(프리로드)에 사용자 정보 포함

 

3. 기술적 의사결정 및 개선 과정

[ SecurityContextHolder 를 활용한 통합된 인증 정보 관리 ]

추출 위치는 정해졌으나, 추출한 정보를 어디에 저장해서 사용할 것인가가 다음 과제였다. 요청의 진입점이 다르다고 해서 저장소까지 분리하게 되면 애플리케이션의 일관성이 떨어지고, 비즈니스 로직에서 처리해야 할 작업이 늘어날 수 있었다.

때문에 기존 Spring Security가 제공하는 SecurityContextHolder를 활용해 통합 관리하기로 했다. 이미 Filter 단에서 SecurityContextHolder를 통해 인증 정보를 관리하고 있었으므로, AOP에서 추출한 정보 역시 이곳에 매핑하면 비즈니스 로직 수정 없이 동일한 방식으로 사용자 정보를 꺼내 쓸 수 있다는 장점이 있었다.

 

[ 비동기 환경에서의 SecurityContext 전파 문제 ]

하지만 이러한 방법은 메세지 발행과 같은 비동기 작업이나 별도의 트랜잭션을 활용해야하는 환경에서는 문제가 발생했다.

SecurityContextHolder는 기본적으로 ThreadLocal 전략을 사용하기 때문에, 요청을 처리하는 스레드가 달라지면(별도의 트랜잭션) 기존 스레드에 저장된 인증 정보를 가져올 수 없었다.

처음에는 이를 해결하기 위해 트랜잭션과 무관한 별도의 저장소(Map 등)에 Key-Value 형태로 저장하는 방식을 고려했으나, 이는 관리 포인트를 증가시키는 비효율적인 방식이란 판단이 들었고, 다른 방법을 모색하기 시작했다.

 

[ 전파 전략 변경 (InheritableThreadLocal) ]

대안으로 SecurityContextHolder의 전략을 자식 스레드에게 컨텍스트를 공유하는 MODE_INHERITABLETHREADLOCAL로 변경하였다. 이를 통해 자식 스레드에서도 부모의 인증 정보를 활용할 수 있게 되었다.

하지만 해당 방식은 심각한 문제가 존재하였다. InheritableThreadLocal은 스레드가 역할을 종료하고 스레드풀로 반환될 때 내부의 정보가 자동으로 비워지지 않았고, 이는 반환된 스레드를 재활용하는 스레드 풀의 특성상

아래와 같은 문제를 유발할 수 있었다.

  1. Dirty Context: 1번 사용자가 썼던 스레드를 2번 사용자가 재사용할 때, 1번 사용자의 인증 정보가 그대로 남아 오동작할 위험이 있다.
  2. Memory Leak: 스레드가 동작 중 오류가 발생해도 컨텍스트 정보가 사라지지 않고 메모리에 점유되는 누수 현상이 발생한다.

 

[ DelegatingSecurityContextAsyncTaskExecutor ]

결국 스레드 풀의 이점을 살리면서도 안전하게 인증 정보를 전파하기 위해 DelegatingSecurityContextAsyncTaskExecutor를 도입했다.

이 Executor는 작업을 래핑(Wrapping)하여 실행한다. 가장 큰 장점은 try-finally 구조를 통해 자식 스레드의 작업이 수행되기 직전에 컨텍스트를 주입하고, 작업이 끝나면 finally 블록에서 확실하게 컨텍스트를 비워준다는(clear) 점이다. 이를 통해 스레드 재사용 시의 정보 오염 문제와 메모리 누수 문제를 깔끔하게 해결할 수 있었다.