Spring Session Redis OOM 장애 원인과 근본 해결법
문제 상황
Spring Session + Redis 환경의 백오피스 시스템에서 로그인 페이지 접근 시 OutOfMemoryError: Java heap space가 발생했다. 서비스 재시작으로 즉시 복구했지만, 재발 방지를 위해 원인을 파고들었다.
에러의 핵심 stack trace는 이렇다:
RedisIndexedSessionRepository.findById()
→ JdkSerializationRedisSerializer.deserialize()
→ DeserializingConverter.convert()
→ OutOfMemoryError: Java heap space
추가로, 최초 OOM 이후 error page를 렌더링하는 과정에서 같은 session을 다시 읽으려다 동일 OOM이 연쇄 발생했다.
원인 분석
직접 원인: 비대한 session payload
JdkSerializationRedisSerializer는 session attribute에 담긴 Java 객체를 기본 직렬화(ObjectOutputStream)로 Redis에 저장한다. 문제는 session에 대용량 객체가 들어가면 Redis에 저장된 값 자체가 수십~수백 MB까지 커질 수 있다는 점이다.
이 상태에서 해당 session을 조회하면 바이트 배열 전체를 heap에 올려 deserialize하므로, 여유 heap이 부족하면 OOM이 터진다.
연쇄 장애 구조
OOM 발생 → Tomcat이 error page(/error)로 forward → SessionRepositoryFilter가 다시 같은 session을 로드 → 같은 OOM 반복. error page 처리가 session에 의존하는 구조에서 빠져나올 수 없는 루프가 만들어진다.
즉시 조치
재시작 외에, 특정 session이 원인이라면 Redis에서 직접 삭제하는 것이 가장 빠르다.
# session 크기 확인
redis-cli MEMORY USAGE "spring:session:sessions:<sessionId>"
# 문제 session 삭제
redis-cli DEL "spring:session:sessions:<sessionId>"
redis-cli DEL "spring:session:sessions:expires:<sessionId>"
근본 해결
1. Session 저장 데이터 최소화
가장 효과가 크고 우선순위가 높은 조치다. session에는 user ID, role 같은 식별 정보만 저장하고, 검색 결과·파일 데이터·캐시성 객체는 별도 저장소(Redis cache, DB)에 두고 key만 session에 보관한다.
// session에 대용량 데이터를 직접 넣지 않는다
session.setAttribute("searchResultKey", cacheKey); // key만 저장
List<Result> results = cacheService.get(cacheKey); // 실제 데이터는 캐시에서 조회
2. Serializer 교체
JdkSerializationRedisSerializer는 바이트 크기가 크고, 클래스 변경 시 serialVersionUID 불일치로 deserialize가 깨지기 쉽다. GenericJackson2JsonRedisSerializer로 교체하면 크기도 줄고, Redis CLI에서 데이터를 직접 확인할 수 있어 디버깅이 편해진다.
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
ObjectMapper om = new ObjectMapper();
om.activateDefaultTyping(
om.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
return new GenericJackson2JsonRedisSerializer(om);
}
주의할 점은 serializer를 교체하면 기존 session 데이터를 읽을 수 없다는 것이다. 배포 시 기존 session을 flush하거나, 두 serializer를 fallback으로 처리하는 래퍼를 만들어야 한다.
3. Session 크기 모니터링
session 크기가 임계값을 넘으면 경고하는 가드를 추가한다. HttpSessionListener를 활용하거나, Redis 레벨에서 주기적으로 크기를 점검하는 스크립트를 돌린다.
redis-cli --scan --pattern "spring:session:sessions:*" | \
xargs -I {} redis-cli MEMORY USAGE {} | sort -n -k1
4. Error Page의 session 의존 제거
error controller가 session 없이도 동작하도록 설계한다. 최소한 error dispatch 경로에서 session 조회를 하지 않도록 하면 연쇄 OOM을 막을 수 있다.
정리
| 조치 | 효과 | 난이도 | |---|---|---| | session 데이터 최소화 | 근본 원인 제거 | 코드 리뷰 필요 | | serializer 교체 | 크기 감소 + 디버깅 편의 | 배포 계획 필요 | | session 크기 모니터링 | 재발 조기 감지 | 낮음 | | error page session 분리 | 연쇄 장애 차단 | 낮음 | | heap 증설 | 임시 완화 | 낮음 |
heap 증설은 시간을 벌어줄 뿐이다. session에 뭘 넣고 있는지 코드를 추적하고, 저장 데이터를 줄이는 것이 핵심이다.