image.png

최근 프로젝트에서 LLM 응답 방식을 개선하기 위해 SSE(Server-Sent Events)를 도입했습니다. 기존에는 API 요청에 대한 응답을 한 번에 내려주었지만, 이제는 LLM의 응답을 Flux 기반의 스트리밍 방식으로 클라이언트에 전달하고 있습니다.

여기서 잠깐 정리하자면,

image.png

즉, LLM 응답을 내려줄 때는 단일 HTTP 요청/응답 구조가 아니라, 하나의 연결을 유지한 채 여러 개의 이벤트를 실시간으로 전달하는 SSE 방식이 사용됩니다.

문제 상황

GIF 2025-09-28 오후 12-44-08.gif

첫 번째 API 요청은 정상적으로 동작했습니다. 이때 Dispatcher TypeREQUEST로 들어왔고, JwtAuthenticationFilter가 정상적으로 동작하면서 쿠키 값을 검증하고 SecurityContextHolder에 인증 정보가 잘 저장되었습니다.

하지만 SSE 응답의 두 번째 이벤트부터 문제가 발생했습니다. 두 번째 요청은 Dispatcher Type이 ASYNC로 들어오면서 기존의 JwtAuthenticationFilter를 거치지 않았고, 그 결과 SecurityContextHolder에 인증 정보가 없는 상태가 되었습니다. 결국, 같은 URL임에도 불구하고 보안 컨텍스트가 비어 있기 때문에 Access Denied 오류가 발생하게 되었습니다.

원인 분석

Spring Security는 요청의 생명주기 동안 SecurityContext를 관리합니다. 일반적인 요청(REQUEST)에서는 필터 체인을 통해 인증 정보가 정상적으로 저장되지만, 비동기 요청(ASYNC)이나 재디스패치(예: FORWARD, INCLUDE)의 경우에는 보안 컨텍스트가 자동으로 전파되지 않을 수 있습니다.

즉, SSE와 같은 비동기 응답 환경에서는 기본적인 보안 설정만으로는 컨텍스트 전파가 보장되지 않기 때문에 Access Denied 문제가 발생할 수 있습니다.

해결 방법

해결책은 SecurityContextRepository를 통해 세션(정확히는 요청 스코프 내 컨텍스트)을 전파하도록 설정하는 것입니다. 이를 위해 Spring Security 5.8+에서 제공하는 다음 옵션을 활용할 수 있습니다.

http.securityContext(context -> context.requireExplicitSave(false));

GIF 2025-09-28 오후 12-54-21.gif

이 옵션을 적용하면 SecurityContext가 변경될 때 자동으로 SecurityContextRepository에 저장되고, 같은 요청 내에서의 재디스패치(REQUEST, ASYNC, FORWARD, INCLUDE) 사이에서도 컨텍스트가 전파됩니다.

SecurityContextRepository란?