개요
처음에는 세션 관리에 대해 "스프링이 알아서 관리해주는 것" 정도로만 생각했다.
하지만 회사에서 인증 서버와 게이트웨이를 직접 구현하게 되면서, 이 '단순하게' 는 나에게 크나큰 어려움으로 다가오게 되었다.
이 글에서는 내가 직접 구현하면서 알게 된 스프링의 세션 관리 방식과, 스프링 세션(Spring Session)이 스프링 시큐리티(Spring Security)와 어떻게 협력하는지에 대해 정리해보려 한다.
세션(Session)이란 뭘까?
HTTP는 기본적으로 Stateless(상태 비저장) 프로토콜이다. Stateless란, 서버가 클라이언트의 상태를 기억하지 않는다는 뜻이다.
즉, 클라이언트가 서버에 요청을 보낼 때마다, 서버 입장에서는 "얘가 누구였더라?" 를 알 수 없는 상태가 된다.
그렇다면 매번 통신할 때마다 내가 누군지 알려야 할까?
그렇다. 매 요청마다 "나 이런 사람이야"라고 신원을 밝히는 과정이 필요하다. 이때 필요한 것이 바로 세션(Session) 이다.
세션은 클라이언트가 서버에 자신을 식별할 수 있는 정보를 전송하는 방법을 제공한다.
하지만 이 세션 정보가 알아서 뿅 하고 서버에 전달되는 것은 아니고... 어떻게 전달될까?
네트워크 탭을 열어보면, HTTP 요청 시 쿠키(Cookie) 가 함께 전송되는 것을 볼 수 있다.
바로 이 쿠키가 세션을 식별하는 역할을 한다.
스프링 mvc의 경우에는 JSESSION-ID 라는 쿠키로 나를 알리고, webflux 의 경우는 SESSION 이라는 쿠키를 통해 "아, 이 클라이언트는 전에 나와 통신했던 그 사람이구나"를 알아본다
그럼 스프링 Session이란 뭘까?
스프링 세션의 공식 사이트 를 보면 알 수 있는데,
Spring Session은 사용자 세션 정보를 관리하기 위한 API와 구현을 제공합니다. 라고 나와있다.
이는 HttpSession, WebSocket, WebSession 세 가지의 방식으로 제공하는데, 여기서 이야기 할 것은 HttpSession과 WebSession을 얘기할 것이다.
구성 모듈로는
- Spring Session Core
- Spring Session Data Redis
- Spring Session JDBC
- Spring Session Hazelcast
- Spring Session MongoDB
이 다섯가지라고 할 수 있다.
어려운 이야기는 좀 멀리하고, 어떤식으로 동작하는지부터 살펴보자.
기본 예제
먼저, 우리는 톰캣 자체의 HttpSession 으로 써보자
@RestController
@RequestMapping("/session")
public class SessionController {
@GetMapping("/save")
public String saveData(HttpSession session) {
session.setAttribute("data", "myData");
// 세션을 저장
return "Data saved in session!";
}
@GetMapping("/get")
public String getData(HttpSession session) {
String data = (String) session.getAttribute("data");
// 세션을 가져오면, Object로 가져오기 때문에 String으로 바꿔준다
return data != null ? "Data from session: " + data : "No data in session.";
}
}
이렇게 하고 브라우저로 실제 url인 http://localhost:8090/session/save에 요청을 해보면


Set-Cookie에 세션 아이디가 설정되어 있는 것을 알수있다.
이것은 톰캣에서, 너 내가 아는 사람이야. 하고 자기가 만든 이름표를 나(브라우저)에게 붙여주는 것이다.
우린 이걸 알아보기 위한 것이 아니니까.. 다시 제대로 된 스프링 세션을 써 보자 (사실 디버깅 하기 전까지는 스프링 세션에서 만든건 줄 암;;)
스프링 세션, 시큐리티를 사용하는 예제
스프링 세션 단독으로 쓰려니까 예제도 많이 없고, 내가 지향하는 것은 시큐리티와 어떻게 협력하는지 동작에 대한 설명이니 간단하게 스프링 시큐리티를 추가해 예제를 만들어보자. 우선 의존성을 추가한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.session:spring-session-core'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
// 프로젝트 만들기를 gradle로 만들었는데, gradle은 잘 몰라서 gradle도 공부해봐야 할듯..
우선 우리는 Spring Security에 대한 Configuraiton부터 작성해줘야 한다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import java.util.concurrent.ConcurrentHashMap;
@Configuration
@EnableSpringHttpSession
// 세션을 쓸 것이라는 명시
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults()); // 기본 폼 로그인 활성화
return http.build();
}
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
간단한 예제이기 때문에, 기본 폼 로그인과 인메모리 세션으로 살펴보도록 하자.
이렇게 설정하고, 애플리케이션을 실행해보면 이런 로그가 뜰텐데
2025-04-14T00:18:42.686+09:00 WARN 27816 --- [StudySession] [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 084ccd0f-210f-44b5-8584-a03cdd91fcee
아이디는 user이고, 여기서 보이는 password가 비밀번호라고 할 수 있다.
이제 이러고 / 루트 컨텍스트에 요청을 해보자. 아마 네트워크 탭에 3가지의 요청이 뜰텐데

세가지인 이유는, 첫 번째는 /로 요청한 요청이 스프링 시큐리티에 의해 거부됐기 때문이다.
아직 우리는 남이니까.. 인증을 하기 위해 302 response로, login 페이지로 보낸 것이다.
css는 아마 로그인 페이지 이쁘게 꾸미려고 불러온 듯..
근데 여기서 주목할 점은, 바로 처음에 말했던 내가 누구인지 알려주는 JSESSIONID가 Set-Cookie에 설정되어 있는 것이다.
왜 벌써부터 세션 아이디가 생긴걸까? 이는 아직 나에 대한 정보는 없지만, 이 요청 자체에 대한 정보를 저장하는 것이다. 실제로 디버깅을 해보면, 아래처럼 모르는 사람인데? 하고 나오는 것을 알 수 있다.
2025-04-14T23:56:27.929+09:00 DEBUG 4304 --- [StudySession] [nio-8090-exec-4] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
근데 세션 얘기하는데 왜 SecurityContext가 나올까? 오늘의 주제가 바로 이 둘이 어떻게 상호작용을 하느냐이다. 참고로 로그인을 해보면 이런 로그를 확인할 수 있다.
2025-04-14T23:58:08.004+09:00 DEBUG 4304 --- [StudySession] [nio-8090-exec-8] o.s.s.a.dao.DaoAuthenticationProvider : Authenticated user
2025-04-14T23:58:08.006+09:00 DEBUG 4304 --- [StudySession] [nio-8090-exec-8] .s.ChangeSessionIdAuthenticationStrategy : Changed session id from 40ef0023-23ce-4beb-a160-8aeaa3a5313a
2025-04-14T23:58:08.007+09:00 DEBUG 4304 --- [StudySession] [nio-8090-exec-8] o.s.s.w.csrf.CsrfAuthenticationStrategy : Replaced CSRF Token
2025-04-14T23:58:08.007+09:00 DEBUG 4304 --- [StudySession] [nio-8090-exec-8] w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=40ef0023-23ce-4beb-a160-8aeaa3a5313a], Granted Authorities=[]]] to HttpSession [org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper@7b370bd9]
2025-04-14T23:58:08.008+09:00 DEBUG 4304 --- [StudySession] [nio-8090-exec-8] w.a.UsernamePasswordAuthenticationFilter : Set SecurityContextHolder to UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=40ef0023-23ce-4beb-a160-8aeaa3a5313a], Granted Authorities=[]]
폼 로그인을 통해서 Spring Security가 인증을 완료하고, 그 인증 완료한 값을 SecurityCotnextImpl이라는 구현체를 만든다.
그럼 세션에서는 이 SecurityCotnextImpl값을 가지고 시큐리티와 상호작용을 한다고 볼 수 있다.
SecurityCotnextImpl 값을 그대로 가지고 있기 때문에 우리가 흔히 인증할 때 사용하는
username, password 등등 정보를 갖고 있는 것을 볼 수가 있다.
이렇듯 스프링에서는 인증을 하면, 인증한 값을 세션에 저장하는 것이 자연스러운 흐름이라고 볼 수 있다.
그럼 스프링은 세션 관리는 어떻게 하는거지?
우선 이 세션을 누가 관리하느냐? 부터 생각해봐야 하는데
package org.springframework.session;
public interface SessionRepository<S extends Session> {
S createSession();
void save(S session);
S findById(String id);
void deleteById(String id);
} // 기존 주석이 너무 많아서 제거했음
이 인터페이스가 세션을 제어하는 주체라고 볼 수 있다.
이 레파지토리를 구현한 구현체들이 (위에서 선언한 MapSessionRepository처럼) 세션을 제어하는 것이다.
워낙 세션 구현체를 사용하는 곳이 여기저기 많아 단편적으로만 얘기하자면,
세션을 처음 만들때는 SessionRepositoryFilter 라는 곳에서 세션에 대한 정보를 불러오면서 시작한다. (이 필터에 바로 들어오는 것은 아니다.)
그리고 여기서 들어오자마자 HttpServletRequest를 심문해 세션이 없으면 createSession을 호출하고,
호출한 Session을 다시 save를 호출해 연결했던 세션을 저장소(메모리 혹은 db)에 저장한다.
(만들고 바로 접속한 정보를 저장한다는게 아니란 뜻)
이 정보는 또 Session이라는 인터페이스를 구현하는 구현체의 attributes에 저장될 것이다.
만약 세션이 있다면 findById를 호출해 이 녀석이 우리가 아는 그 녀석이구나!
(인증 됐는지 안 됐는지는 다른 얘기임) 를 알고, save 함수를 호출해 이녀석이 내가 관리하는 녀석이다! 라고 저장소에 저장할 것이다.


나는 당연히 세션을 만들면 만드는 그 순간에 저장하고 조회하는 순간에 저장하고..
그냥 무언가를 할 때마다 저장하는 줄 알았는데 책임분리가 이렇게 잘 되어있는지 몰랐다.
저장소에 저장하는 저 로직을 분리했기 때문에 rdbms 혹은 nosql에 저장하는 로직으로 대체되더라도,
SessionRepositoryFilter에서는 아무것도 모르고 쓸 수 있는 것이기도 하다.
그럼 이게 뭐라고 기록하냐? 라고 물어보면 그냥 이번에 이 세션 저장소를 커스터마이징하면서
이런 기술에 대한 딥다이브는 처음이었는데, 꽤나 재미도 있었다.
딥하게 들어가놓고 까먹으면 아쉬우니깐.. 기록하는 습관도 들일 겸...
다음 편에는 내가 왜 deepDive를 하게 됐는지에 대한 생각과,
그리고 그 이유인 db에 저장하는 방법을 보려고 했는데...
세션을 redis에 저장하는것이 일반적이기 때문에 redis에 저장하는 플로우를 보도록 하겠다.
'Spring' 카테고리의 다른 글
레거시 전자정부 프레임 워크의 프로퍼티 값 관리 (0) | 2025.04.17 |
---|