[Redis] Spring Session 분산 세션: 설정, 세션 만료 TTL, 세션 vs JWT 비교
![[Redis] Spring Session 분산 세션: 설정, 세션 만료 TTL, 세션 vs JWT 비교](https://blog.kakaocdn.net/dna/b4QRSw/dJMcah5MVze/AAAAAAAAAAAAAAAAAAAAAICuz2f0P7wLxiHXqw4vbcrW8-o07s59dFcC5KegzsVu/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1785509999&allow_ip=&allow_referer=&signature=f8865jxmEcrRqcndyuPXEu2l66M%3D)
[Redis] Spring Session 분산 세션: 설정, 세션 만료 TTL, 세션 vs JWT 비교
서버가 한 대면 세션은 그 서버 메모리에 두면 돼요. 그런데 서버가 여러 대가 되면 문제가 생겨요. 로그인은 A 서버에서 했는데 다음 요청이 B 서버로 가면, B는 그 세션을 몰라서 로그인이 풀려요. 이걸 해결하는 게 세션을 모든 서버가 공유하는 곳에 두는 거고 그 저장소로 Redis를 많이 써요.
분산 환경에서 세션이 왜 문제가 되는지부터 보고, Spring Session + Redis를 설정하고 그 동작 원리를 뜯어봐요. 그다음 직렬화와 만료를 챙기고, 끝으로 세션과 토큰(JWT)을 나란히 비교해요. Redis 기본은 Spring Data Redis 설정 글을 먼저 보면 좋아요.
01. 분산 환경의 세션 문제
전통적인 세션은 서버 메모리(예: 톰캣)에 저장돼요. 서버가 한 대면 괜찮은데, 로드밸런서 뒤에 여러 대가 있으면 요청마다 다른 서버로 갈 수 있어요. 그러면 세션이 있는 서버와 요청이 가는 서버가 달라서 로그인이 풀려요.

임시방편으로 sticky session(같은 사용자는 늘 같은 서버로)을 쓰기도 해요. 그런데 이러면 그 서버가 죽으면 세션도 같이 날아가고 부하 분산도 고르지 않아요. 배포로 서버를 내렸다 올리면 세션이 전부 사라지고요. 그래서 근본 해결은 세션을 서버 밖 공유 저장소에 두는 거예요.
02. Spring Session + Redis
Spring Session은 세션 저장소를 추상화해줘요. Spring Boot면 의존성 하나만 추가하면 끝이에요. 자동 설정이 알아서 구성해줘서 기존 HttpSession 코드를 그대로 두고 저장소만 Redis로 바뀌어요.
// 의존성: spring-boot-starter-session-data-redis
// Boot면 별도 설정 클래스 없이 이걸로 끝이에요.
// 주의: @EnableRedisHttpSession을 직접 붙이면 Boot 자동 설정이 물러나면서
// spring.session.timeout 프로퍼티가 무시돼요 (타임아웃이 애너테이션 속성으로만 제어됨)
이게 핵심이에요. 우리는 여전히 session.setAttribute(...)를 쓰지만 실제 저장은 Redis로 가요. 코드를 거의 안 바꾸고 분산 세션이 되는 거죠. 모든 서버가 같은 Redis를 보니, 어느 서버로 요청이 가든 같은 세션을 읽어요.
03. 어떻게 동작하나

흐름은 이래요. 사용자가 로그인하면 세션 ID가 만들어져 쿠키로 내려가요. 세션 데이터(로그인 정보 등)는 Redis에 그 ID를 키로 저장돼요. 다음 요청이 오면 쿠키의 세션 ID로 Redis에서 세션을 찾아 읽어요.
그래서 서버는 세션을 들고 있지 않아요. 세션 ID만 주고받고 실제 데이터는 Redis에 있어요. 이러면 서버를 늘리거나 줄여도, 배포로 내렸다 올려도 세션이 유지돼요. 서버는 상태가 없어지고(stateless) 상태는 Redis가 들고 있는 구조예요.
04. 세션 직렬화
세션 데이터도 Redis에 저장되니 직렬화 문제를 만나요. 기본은 JDK 직렬화라, 세션에 담는 객체가 Serializable이어야 하고 사람이 못 읽는 바이트로 저장돼요.
JSON으로 저장하고 싶으면 직렬화기를 바꿔요. Spring Data Redis 설정 글에서 본 직렬화 함정과 같아요. 단 세션 객체는 보안 정보가 담기니, 직렬화 방식을 바꿀 때 호환성과 보안을 같이 봐야 해요.
05. 세션 만료
세션은 만료가 중요해요. Spring Session은 세션 타임아웃을 Redis의 TTL로 관리해요. 설정한 시간 동안 활동이 없으면 Redis에서 자동으로 사라져요.
spring:
session:
timeout: 30m # 30분 무활동 시 만료
이게 메모리 세션보다 깔끔한 점이에요. 만료 관리를 Redis의 TTL에 맡기니, 서버가 세션 청소 로직을 따로 안 돌려도 돼요. 다만 활동할 때마다 TTL을 갱신하는 동작이 있어서 트래픽이 아주 크면 그 갱신 비용도 고려해요.
세션 이벤트의 함정 하나. Spring Session은 세션 생성·삭제·만료 이벤트를 발행해서 "만료 시 접속자 수 감소" 같은 후처리를 걸 수 있는데, Redis의 만료 이벤트는 키에 접근해야 발생하는 경우가 있어 정확한 타이밍이 보장되지 않아요. 만료 후 한참 뒤에 이벤트가 올 수도 있어요. 정밀한 타이밍이 필요한 로직(과금 마감 등)을 만료 이벤트에 의존시키면 안 돼요.
06. 동시 세션 제어와 보안
세션을 Redis로 모으면 좋은 점이 하나 더 있어요. 한 사용자의 세션을 전부 한곳에서 볼 수 있으니, 동시 로그인을 제어할 수 있어요. "한 계정은 한 기기만"처럼 새 로그인 시 기존 세션을 끊거나, 반대로 새 로그인을 막을 수 있어요. Spring Security와 함께 쓰면 이런 동시 세션 정책을 걸 수 있어요.
보안에서 또 챙길 게 세션 고정(session fixation) 공격이에요. 로그인 전후로 세션 ID를 바꿔주면, 공격자가 미리 심어둔 세션 ID로 인증을 가로채는 걸 막아요. Spring Security는 로그인 시 세션 ID를 새로 발급하는 게 기본이라, 그 동작이 켜져 있는지 확인하면 돼요.
그리고 세션 ID를 담는 쿠키는 HttpOnly·Secure로 둬요. 자바스크립트가 못 읽게(HttpOnly), HTTPS에서만 전송되게(Secure) 해서 세션 탈취 위험을 줄여요. 세션을 Redis로 옮기는 김에 이런 쿠키 보안도 같이 점검하면 좋아요.
07. 세션 vs 토큰(JWT)
분산 환경에서 인증 상태를 다루는 방법은 크게 둘이에요. Redis 세션과 JWT 토큰이에요.

- Redis 세션 — 상태를 서버(Redis)가 들고 있어요. 세션을 서버에서 바로 무효화할 수 있어요(강제 로그아웃). 대신 매 요청마다 Redis를 조회해요.
- JWT 토큰 — 상태를 토큰 자체에 담아요. 서버 조회 없이 검증돼 빠르지만 한 번 발급하면 만료 전엔 무효화가 어려워요.
그래서 "즉시 무효화(강제 로그아웃·차단)가 중요하면 세션, 조회 없는 확장성이 중요하면 토큰"이 대략의 기준이에요. 둘을 섞어서 토큰을 쓰되 무효화 목록(블랙리스트)만 Redis로 관리하는 절충도 흔해요. 정답은 없고 요구사항에 맞춰 골라요.
08. 자주 만나는 문제
로그인이 가끔 풀려요
일부 요청이 세션을 공유 못 하는 거예요. Spring Session이 제대로 활성화됐는지, 모든 서버가 같은 Redis를 보는지 확인해요.
세션 직렬화에서 예외가 나요
세션에 담은 객체가 직렬화 불가이거나 직렬화기가 안 맞는 거예요. Serializable 여부와 직렬화 설정을 맞춰요.
Redis가 죽으면 다 로그아웃돼요
세션을 Redis에만 의존하니 Redis 장애가 곧 로그인 풀림이에요. 그래서 세션 저장용 Redis는 고가용성으로 두는 게 중요해요. 고가용성 구성은 메모리 정책과 고가용성 편에서 다뤄요.
세션이냐 토큰이냐
분산 세션은 세션을 서버 밖 공유 저장소에 두어, 어느 서버로 요청이 가든 같은 세션을 읽게 하는 것이에요. Spring Session + Redis면 기존 HttpSession 코드를 거의 안 바꾸고 분산 세션이 돼요. 직렬화와 만료(TTL)만 챙기면 돼요.
남는 선택은 인증 상태를 어디에 둘지예요. 즉시 무효화(강제 로그아웃·차단)가 중요하면 매 요청 Redis를 조회하더라도 세션이 낫고 조회 없는 확장성이 중요하면 토큰이에요. 둘을 섞어 토큰을 쓰되 무효화 목록만 Redis로 관리하는 절충도 흔하니, 요구사항에 맞춰 고르면 돼요. 메시지 처리로 넘어가면 Redis Streams가 기다려요.
'백엔드 > 캐시' 카테고리의 다른 글
| [Redis] 메모리 정책과 고가용성: maxmemory eviction, RDB vs AOF, Sentinel, Cluster (0) | 2026.07.01 |
|---|---|
| [Redis] Streams 메시지 큐: 소비자 그룹, XACK, PEL 재처리 그리고 Kafka 비교 (0) | 2026.06.30 |
| [Redis] 레이트 리미팅 구현: 고정 윈도, 슬라이딩 윈도, 토큰 버킷 비교와 Lua 원자화 (0) | 2026.06.26 |
| [Redis] 캐시 스탬피드, 관통, 사태: 원인과 해결 방법 (TTL jitter, 뮤텍스, Bloom Filter) (0) | 2026.06.25 |
| [Redis] 분산 락 구현: SETNX, Lua 안전 해제, Redisson watchdog, Redlock 한계 (0) | 2026.06.24 |
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."