2022. 6. 29. 00:59ㆍSpring/Session
1. 개요
Spring Session은 Spring 애플리케이션에서 세션을 더 통합적이고 체계적으로 관리하기 위해 개발된 프로젝트이다. Spring Session은 HTTP, WebSocket, WebFlux 환경에서 언제든지 세션과 관련된 구현체를 변경할 수 있도록 제공된다.
2. 모듈
Spring Session을 활용하기 위해 제공되는 주요 모듈을 살펴보자.
- Spring Session Core - 핵심 Spring Session 기능 및 API 제공
- Spring Session Data Redis - Redis 구성을 지원하는
SessionRepository
및ReactiveSesionRepository
구현체 제공 - Spring Session JDBC - RDB 및 구성을 지원하는
SessionRepository
구현체 제공
3. Spring Session Core 톺아보기
Spring Session Core 라이브러리를 한 번 살펴보도록 하자.
EnableSpringHttpSession
이 어노테이션을 추가하면 사용자가 만든 SessionRepoisitory
구현체에서 지원하는springSessionRepositoryFilter
라는 이름의 스프링 빈으로 SessionRepositoryFilter
가 생성된다. 따라서 이 어노테이션을 사용하려면 SessionRepository
빈을 반드시 만들어야 한다.
@Configuration
@EnableSpringHttpSession
public class SpringHttpSessionConfig {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
그렇다면 SessionRepositoryFilter
와 SessionRepository
는 뭘까? 먼저 SessionRepositoryFilter
부터 알아보자.
SessionRepositoryFilter
이 필터는 SessionRepository
에서 지원하는 HttpSession
의 구현체를 사용하여 HttpServletRequest
를 래핑하는 책임을 수행한다. 글로만 보면 와닿지 않을 것이니 코드를 살펴보도록 하자.
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
finally {
wrappedRequest.commitSession();
}
// ...
}
SessionRepositoryFilter
는 위와 같이 HTTP 요청과 응답을 각각 SessionRepositoryRequestWrapper
와 SessionRepositoryResponseWrapper
로 한 번 감싼 후 filterChain에 태워보낸다. 이렇게 함으로써 필터를 거친 후 우리가 사용하는 인터셉터, 컨트롤러 등에서 부가적인 기능을 사용할 수 있게 되는 것이다. 로직을 수행한 후에 다시 이 필터로 돌아왔을 때 warppedRequest.commitSession()을 통해 세션을 저장한다.
SessionRepositoryRequestWrapper
클래스의 핵심 부분을 살펴보면 다음과 같다.
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
}
else {
S session = wrappedSession.getSession();
clearRequestedSessionCache();
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
// ...
}
commitSession()은 현재 세션을 검증하고 세션 저장소에 저장하는 역할을 하는 메서드이다. 여기서 반환값이 HttpSessionWrapper
인데 조금 있다가 알아보기로 하자.
이 메서드의 처리 과정은 간략히 다음과 같다.
- getCurrentSession()을 통해 현재 세션을 가져온다.
- 값이 없다면 클라이언트 세션의 유효성 검증을 수행한다.
- 값이 들어있다면 세션 저장소에 세션을 저장한다. 그 후에 응답으로 내려줄 세션 ID를
SessionRepositoryFilter
의httpSessionIdResolver
를 통해 저장한다.
💡 `HttpServletRequestWrapper`는 `HttpServletRequest`를 최소한으로 구현한 구현체로 개발자들이 이를 상속해서 쉽게 사용하도록 서블릿 표준으로 지원하는 클래스다.
위 과정을 정리하면 다음과 같다.
EnableSpringHttpSession
어노테이션을 사용하여SessionRepositoryFilter
를 생성한다. 이때SessionRepository
빈을 꼭 함께 생성해야 한다.- 생성한 후에 요청들은 필터에서 래퍼 클래스에 의해 한 번 감싸진다.
SessionRepositoryFilter
의 Order는Integer.MIN_VALUE + 50
이다. 이렇게 필터의 앞쪽에 배치되는 이유는 이후의 필터에서도 래핑된 request, response를 사용할 수 있도록 하기 위함이다. - 웹 서비스 로직이 모두 수행되고 다시 이 필터로 돌아왔을 때 warppedRequest.commitSession()을 통해 세션을 저장하고 응답에 세션 ID를 세팅한다.
SessionRepository
이름에서 알 수 있듯이 세션 저장소에 대한 인터페이스이다. Spring Session에서는 세션 저장소를 이 인터페이스의 구현체를 통해 세션을 저장 및 관리한다. MapSessionRepository
는 Spring Session Core 라이브러리에서 제공하는 기본적인 SessionRepository
구현체다. 다시 말하면 SessionRepository
인터페이스를 구현하여 빈으로 등록하기만 하면 해당 구현체로 Spring Session Core를 사용할 수 있는 것이다. Redis, Mongo 등과 같은 것도 마찬가지이다.
CookieSerializer
EnableSpringHttpSession
을 살펴보면 @Import(SpringHttpSessionConfiguration.class)
구문을 볼 수 있다. SpringHttpSessionConfiguration
은 웹 환경에서 Spring Session의 기본적인 세팅을 설정하는 클래스이다. 이 클래스에서 살펴볼 것은 CookieSerializer
이다.
기존에 톰캣에서 세션을 관리할 때는 세션 쿠키의 키는 JSESSIONID로 세팅되어 내려왔지만 Spring Session을 적용하고 나면 키가 SESSION으로 바뀐다. Spring Session에서는 CookieSerializer
인터페이스를 두고 구현체를 통해 세션 쿠키를 처리한다. Spring Session Core에서는DefaultCookieSerializer
라는 클래스로 기본 구현체를 제공한다. 핵심 코드를 잠깐 훑어보고 가자.
public class DefaultCookieSerializer implements CookieSerializer {
private String cookieName = "SESSION";
private boolean useHttpOnlyCookie = true;
private String sameSite = "Lax";
// 기타 다른 필드들
@Override
public List<String> readCookieValues(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
List<String> matchingCookieValues = new ArrayList<>();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (this.cookieName.equals(cookie.getName())) {
String sessionId = (this.useBase64Encoding ? base64Decode(cookie.getValue()) : cookie.getValue());
if (sessionId == null) {
continue;
}
if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
sessionId = sessionId.substring(0, sessionId.length() - this.jvmRoute.length());
}
matchingCookieValues.add(sessionId);
}
}
}
return matchingCookieValues;
}
@Override
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
StringBuilder sb = new StringBuilder();
sb.append(this.cookieName).append('=');
String value = getValue(cookieValue);
if (value != null && value.length() > 0) {
validateValue(value);
sb.append(value);
}
// 쿠키 옵션 설정 로직 생략
response.addHeader("Set-Cookie", sb.toString());
}
// ...
}
HttpSessionWrapper
이 클래스는 HttpSessionAdapter
를 상속하고 invalidate()만 오버라이딩한 클래스다. 실제로 HttpSession
을 구현한 클래스는 HttpSessionAdapter
이다.
HttpSessionAdapter
이 클래스가 실제로 Spring Session의 Session
을 HttpSession
으로 변환해주는 역할을 한다. Spring Session에서는 세션을 Session
인터페이스의 구현체로 처리한다. SessionRepositoryFilter
의 선언부의 제네릭을 보면 <S extends Session>
을 볼 수 있다. Spring Session Core에서는 Session
의 구현체로 MapSession
을 제공한다.
public class SessionRepositoryFilter**<S extends Session>** extends OncePerRequestFilter {
💡 Adapter로 끝나는 클래스들은 디자인 패턴 중 Adapter 패턴을 따른 클래스로 유추할 수 있다. 이 글에서는 디자인 패턴을 다루지 않으므로 필자의 다른 글을 참고하면 좋겠다.
Spring Session 핵심 인터페이스 정리
- 세션을 Session 인터페이스로 다루며
HttpSession
을 구현하는HttpSessionAdapter
를 통해 변환한다. Core에서는MapSession
을 제공한다. - 세션 저장소는
SessionRepository
인터페이스로 다루며 Core에서는MapSessionRepository
를 제공한다. - 세션 쿠키는
CookieSerializer
인터페이스로 다루며 Core에서는DefaultCookieSerializer
를 제공한다. 이 클래스에서 쿠키 이름을 SESSION으로 지정하기 때문에 세션 ID 키가 JSESSIONID에서 SESSION으로 바뀐다.
이벤트
Spring Session에서는 세션의 생성, 삭제, 제거, 만료 이벤트를 제공한다. 따라서 Spring Session을 이용할 때 해당 이벤트 발생 시 처리해야 하는 로직이 있다면 상황에 맞는 이벤트를 사용하자.
abstract AbstractSessionEvent extends ApplicationEvent
- 세션 ID와 세션을 가지고 있다.
SessionCreatedEvent extends AbstractSessionEvent
- 세션이 생성되었을 때 발생하는 이벤트
SessionDeletedEvent extends AbstractSessionEvent
- 세션이 삭제를 통해 파괴되었을 때 발생하는 이벤트
SessionDestroyedEvent extends AbstractSessionEvent
- 세션이 파괴되었을 때 발생하는 이벤트
SessionExpiredEvent extends AbstractSessionEvent
- 세션이 만료되었을 때 발생하는 이벤트
정리
- Spring Session는
@EnableXXXHttpSession
어노테이션을 적용하고SessionRepository
인터페이스를 구현한 빈을 생성하여 사용할 수 있다. - Spring Session에서는 세션을
Session
인터페이스로 다루며 세션 저장소는SessionRepository
로 다룬다. SessionRepositoryFilter
는springSessionRepositoryFilter
라는 이름의 빈으로 등록되며 순서는 필터의 앞단에 배치되어 이후의 필터 및 서비스 로직에서 부가 기능을 사용할 수 있다.
다시 돌아보면 Spring Session이 제공하는 것은 심플하다.
- 세션과 관련된 것들을 인터페이스화하여 다형성을 제공한다.
- 필터에서 요청과 응답을 감싸 부가 기능을 제공한다.
Spring Session Core를 이해했다면 Spring Session Data Redis나 Mongo, JDBC와 같은 라이브러리도 쉽게 이해할 수 있을 것이다. 이 글을 쓰게된 이유도 프로젝트에서 Redis로 세션 관리를 하려다 내부에서 도저히 무슨 일이 일어나는지 모르겠어서였다. 공식 문서만으로는 융통성이 부족한 내 머리로 이해할 수 없었다… 이 글이 나같이 코드를 직접 뜯어봐야 이해가 되는 사람들에게 도움이 되었으면 한다.