전환 자체는 하루였다
- 내용: JWT 기반 인증 → Redis 기반 Spring Session
- 삭제:
JwtFilter,JwtProvider등 클래스 6개, 374줄 -
커밋 하나로 종료
- 정작 내용이 있는 구간: 그다음 3주
- 이 글의 주제: 세션 전환 후 드러난 문제들
(a) API 호출을 세션 갱신 신호로 쓰기
- 세션 기반 전환 → 만료 관리 필요
-
후속 작업에서
SessionSyncResponseAdvice신설 -
핵심 아이디어: 별도의 갱신 엔드포인트를 두지 않는 것
- 전제: 사용자가 API를 호출한다는 사실 자체가 “활동 중” 신호
- 구현:
ResponseBodyAdvice로 모든 응답을 가로채 세션 타이머 갱신 -
남은 시간은
X-Session-Remaining헤더로 전달 - 프런트엔드가 이 헤더를 읽으려면 CORS 노출 헤더 등록 필요
- 기본 동작: 브라우저는 안전 목록에 없는 응답 헤더를 JS에 미노출
- 증상: 서버가 헤더를 보내도 프런트에서
undefined - 조치:
exposedHeaders에 추가
폴링용 keep-alive 엔드포인트를 만드는 방식보다 나은 점은, 유휴 상태를 유휴로 판정한다는 것이다.
- 탭만 열어두고 아무것도 안 하는 사용자 — 폴링 방식에서는 세션 무한 유지
- 이 방식에서는 정상 만료
(b) 동시 요청과 Redis 장애
- 한 커밋에서 수정한 두 가지
원자적 연산으로 전환
- 문제: 세션 동기화 상태를 Redis에 기록할 때 “읽고 → 없으면 쓰기” → 동시 요청에서 레이스
- 브라우저의 병렬 API 호출은 흔한 상황이라 실제로 걸림
- 조치:
setIfAbsent(RedisSETNX) → 한 번의 원자적 연산
Redis 장애 가드
- 세션 갱신은 부가 기능
- Redis 다운 → 응답 어드바이스에서 예외 → 모든 API 응답 실패
- 부가 기능이 주 기능을 죽이는 구조
-
조치: 예외를 잡아 갱신만 건너뛰는 가드 추가
- 응답 후처리에 무언가를 얹을 때의 필수 확인: 그 실패가 응답 전체를 막는지 여부
(c) 배포하니 로그인이 500
-
가장 고전적인 함정
- Redis 저장 세션 = 자바 객체의 직렬화 결과
- 배포로 세션에 담기는 클래스 구조 변경 → 이전 버전이 만든 세션은 역직렬화 실패
- 기존 세션 정리 로직(
invalidateExistingSessions())이 역직렬화를 시도하다SerializationException발생 - 결과: 로그인 시 500
- 로그인 불가 → 새 세션 생성도 불가
-
구조: 낡은 세션이 새 로그인을 막는 교착
- 해결: 예외를 잡아 역직렬화 없이 Redis 인덱스 키를 직접 삭제
- 근거: 세션 내용을 읽을 수 없어도 키는 삭제 가능
-
요점: 객체로 복원하지 않고 키 레벨에서 처리
- 세션에 도메인 객체를 담을 경우: 배포마다 이 위험 동반
- 안전한 방향: 담는 것 최소화 + 정리 경로를 역직렬화 비의존으로 구성
(d) WebSocket과 권한 변경
HTTP는 요청마다 세션을 확인하지만 WebSocket은 연결이 한 번 맺어지면 계속 살아있다.
-
즉 연결 시점의 인증만으로는 불충분
- 조치: STOMP
SUBSCRIBE시점에 세션 재검증 - 근거: 구독은 특정 주제의 데이터를 받겠다는 요청 → 그 시점의 세션 유효성·권한 확인 필요
권한 변경 처리:
- 문제: 관리자가 권한을 낮춰도 해당 사용자 세션이 살아있으면 즉시 미반영
-
조치: 권한 변경 이벤트 발생 시 강제 로그아웃 이벤트 전송
- 관련 커밋 — 캐시 무효화 버그 수정
- 기존 동작: 권한 변경 시 Spring
CacheManager의 사용자 캐시만 삭제 -
누락: Redis에 따로 둔 권한 캐시 키
- 원칙: 캐시가 두 곳이면 무효화도 두 곳 — 한쪽만 지우면 어긋난 상태 잔류
(e) 쿠키 속성 정합성
- 세션 쿠키 속성:
SameSite,Secure, 도메인 - 환경별로 달라야 함 — 로컬은
Secure불가, 운영은 필수 -
yml에 흩어져 있으면 조합이 어긋나기 쉬움
- 대표 사례:
SameSite=None+Secure=false - 브라우저가 이 쿠키를 거부
- 증상: 로그인이 되는 것처럼 보이다가 다음 요청에서 인증이 풀린다
-
서버 로그에는 에러가 없어서 원인 찾기가 오래 걸리는 종류
- 조치:
@ConfigurationProperties로 쿠키 설정을 한 클래스에 통합 - 부팅 시 불일치 조합이면 경고 로그
- 부팅은 막지 않음 — 로컬 개발 편의를 위해 의도적으로 어긋나게 두는 경우가 있기 때문
남는 교훈
| 영역 | 증상 | 처방 |
|---|---|---|
| 세션 갱신 | 별도 폴링 필요 | API 호출을 갱신 신호로 |
| 동시성 | 병렬 요청 레이스 | setIfAbsent 원자 연산 |
| Redis 장애 | 전체 API 실패 | 부가 기능 예외 격리 |
| 배포 | 로그인 500 | 인덱스 키 직접 삭제 |
| WebSocket | 권한 변경 미반영 | SUBSCRIBE 재검증 + 강제 로그아웃 |
| 쿠키 | 조용한 인증 실패 | 설정 통합 + 불일치 경고 |
JWT에서 세션으로 옮기는 건 흔히 “상태를 서버로 되가져오는” 선택으로 설명된다.
-
맞는 설명이지만 실제 비용이 든 지점: 그 상태를 어디에 두고, 언제 갱신하고, 실패하면 어떻게 되는가
- JWT — 검증이 자족적이라 이런 질문이 적음. 대신 무효화가 어려움
-
세션 — 무효화가 쉬움. 대신 저장소가 단일 장애점, 직렬화가 배포와 결합, 갱신 정책을 직접 결정
- 3주 동안 확인한 것: 어느 쪽이 낫다기보다 비용이 나타나는 위치가 다르다