← 개발일지

Spring Session Redis OOM: Root Cause and Permanent Fix


The Problem

A back-office application running Spring Session with Redis started throwing OutOfMemoryError: Java heap space on login page requests. A restart fixed it immediately — but the real question was why it happened and how to prevent it from coming back.

The stack trace pointed straight at the culprit:

RedisIndexedSessionRepository.findById()
  → JdkSerializationRedisSerializer.deserialize()
    → DeserializingConverter.convert()
      → OutOfMemoryError: Java heap space

To make things worse, the initial OOM triggered a cascade: Tomcat forwarded to the error page, which tried to load the same session again, causing the same OOM in a loop.

Root Cause

Oversized Session Payload

JdkSerializationRedisSerializer uses Java's built-in ObjectOutputStream to serialize session attributes into Redis. When someone stores a large object in the session — a massive list, file bytes, cached query results — the serialized payload in Redis can balloon to tens or hundreds of megabytes.

When that session gets loaded, the entire byte array gets pulled into the JVM heap for deserialization. If there isn't enough headroom, you get an OOM.

The Cascade Effect

OOM → Tomcat forwards to /errorSessionRepositoryFilter reloads the same session → same OOM. If your error page depends on the session in any way, there's no escape.

Quick Fix

If a specific session is the culprit, delete it directly from Redis:

# Check session size
redis-cli MEMORY USAGE "spring:session:sessions:<sessionId>"

# Remove it
redis-cli DEL "spring:session:sessions:<sessionId>"
redis-cli DEL "spring:session:sessions:expires:<sessionId>"

Or restart the service if you can afford the session loss.

Permanent Fixes

1. Minimize What You Store in Sessions

This is the highest-impact change. Store only identifiers (user ID, role) in the session. Everything else — search results, file data, cached objects — goes into a separate store (Redis cache, database), and you keep just the lookup key in the session.

// Don't do this
session.setAttribute("searchResults", hugeList);

// Do this instead
session.setAttribute("searchResultKey", cacheKey);
List<Result> results = cacheService.get(cacheKey);

2. Replace the Serializer

JdkSerializationRedisSerializer is the default, but it's a poor choice for production. The serialized bytes are large (they include full class metadata), opaque (binary, unreadable in Redis CLI), and fragile (serialVersionUID mismatches break deserialization after class changes).

Switch to GenericJackson2JsonRedisSerializer:

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
    ObjectMapper om = new ObjectMapper();
    om.activateDefaultTyping(
        om.getPolymorphicTypeValidator(),
        ObjectMapper.DefaultTyping.NON_FINAL
    );
    return new GenericJackson2JsonRedisSerializer(om);
}

One caveat: changing the serializer means existing sessions can't be deserialized. You'll need to flush all sessions during deployment, or write a fallback wrapper that tries both serializers.

3. Monitor Session Sizes

Add a guard that warns you before sessions get dangerously large:

redis-cli --scan --pattern "spring:session:sessions:*" | \
  xargs -I {} redis-cli MEMORY USAGE {} | sort -n -k1

You can also add an HttpSessionListener that logs a warning when any attribute exceeds a size threshold.

4. Decouple Error Pages from Sessions

Make your error controller work without touching the session. At minimum, ensure the error dispatch path doesn't trigger session lookups. This breaks the cascade loop even if an OOM occurs elsewhere.

Summary

| Fix | Impact | Effort | |---|---|---| | Trim session data | Eliminates root cause | Code audit needed | | Replace serializer | Smaller payloads + debuggability | Deployment planning | | Monitor session sizes | Early warning | Low | | Session-free error pages | Stops cascade failures | Low | | Increase heap | Temporary relief only | Low |

Bumping the heap is a band-aid. The real fix is auditing what goes into the session and keeping it lean.