🔍 트러블 슈팅
✏️Key가 두 개 생성되는 문제
세션을 저장하는 로직은 다음과 같다.
@Component
class SessionFilter(
private val redisSessionRepository: RedisSessionRepository,
): OncePerRequestFilter() {
val log = logger()
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val session = getSessionFromCookie(request) --- 1
if (session == null) {
val sessionId = request.getSession(true).id --- 2
myRedisSessionRepository.setSession(sessionId) --- 3
}
filterChain.doFilter(request, response)
}
}
1. 쿠키에 세션에 대한 정보가 있는지 확인한다.
2.세션이 없다면 새로운 세션을 생성한다.
3.새로 발급받은 세션을 레디스에 저장한다.
결과를 보면 세션이 두 개가 저장되었음을 확인할 수 있다. 첫 번째는 위의 코드에 의해 발급된 것이고 2번 세션은 왜 발생한 것일까?
이를 해결하기 위해 디버깅 모드로 코드를 하나씩 따라 가 봤다.
- getSession()을 호출하면 HttpServletRequestWrapper의 getSession이 호출된다.
class HttpServletRequestWrapper {
@Override
public HttpSession getSession(boolean create) {
return this._getHttpServletRequest().getSession(create);
}
}
- SessionRepositoryFilter가 동작한다.
@Override
public HttpSessionWrapper getSession(boolean create) {
// 1. Session이 있는지 확인합니다.
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
// 2. Session을 생성하고 등록합니다.
S session = SessionRepositoryFilter.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
- RedisSessionRepository가 동작한다.
public RedisSessionRepository {
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
RedisSession session = new RedisSession(cached, true);
session.flushIfRequired(); //
return session;
}
}
여기서 flush모드가 yes라면 곧바로 캐시에 업데이트 된다… 아니라면 응답을 보내기 전에 DispatcherServlet에서 save() 메서드를 호출하여 저장하는 작업을 거친다. 즉, 세션을 새로 발급하면 SessionRepositoryFilter가 동작하여 Redis에 세션을 저장해준다.
✏️ 쿠키와 Base64 인코딩
웹 브라우저에서 확인해보면 SESSION이 인코딩되어서 들어간 것을 확인할 수 있다. (세션이 두 개 있는데 위의 글을 읽으면 해결할 수 있다.)
configuration에서 session 생성을 false로 두면 된다고 하는데 잘 동작 되지 않았다.(스프링이 자동으로 넣어주는 기능을 끄니까 아마 제대로 동작했다면 위의 문제도 같이 해결되었을 것이다.) 다음 링크에서 힌트를 얻어 DefaultCookieSerializer부터 디버깅 모드로 하나씩 찾아나갔다.
https://stackoverflow.com/questions/51517246/whats-the-difference-between-cookie-session-and-session-id-in-database-for-spri
val sessionId = request.getSession(true).id
다음 코드를 호출하는 순간 RedisSessionRepository에서 세션을 생성하고 이를 현재 세션으로 넣은 다음 헤더에 추가해준다. 즉, 추가적으로 header에 담는 로직이 필요 없다. 실제로 세션이 2개가 들어왔지만 하나는 base64로 인코딩 한 값이다.
이를 추적하면서 뽑아낸 중요한 코드 리스트다.
- Session이 있는지 확인한다.
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
// 현재 세션이 존재하면 return
if (currentSession != null) {
return currentSession;
}
// 캐시되어 있는 세션이 있는지 확인
S requestedSession = getRequestedSession();
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.markNotNew();
setCurrentSession(currentSession);
return currentSession;
}
}
// create의 속성이 fasle라면 세션을 생성하지 않는다.
if (!create) {
return null;
}
// create = true라면 새로운 세션을 생성한다.
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
- 지정한 스토리지에서 Session을 생성한다. (flush모드에 따라 저장 시점이 다르다)
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
RedisSession session = new RedisSession(cached, true);
session.flushIfRequired();
return session;
}
- 이후 Response 보내기 전에 세션을 Base64로 인코딩하여 header에 저장한다.
@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId));
}
@Override
public void writeCookieValue(CookieValue cookieValue) {
String value = getValue(cookieValue); // getValue()에서 인코딩함.
...
response.addHeader("Set-Cookie", sb.toString()); // 헤더에 저장
}
✏️ 결론
- 스프링 세션을 적용하면 session이 생성되는 시점에 데이터를 저장해준다.
- 세션을 쿠키로 반환할 때는 Base64로 인코딩하면 반환한다.