Redis를 캐시로만 보면 놓치는 것들

Redis의 자료구조, TTL과 eviction, Sentinel과 Cluster, Pub/Sub/List/Stream 큐 모델, Spring Boot에서 Lettuce를 사용할 때의 설정과 주의사항을 정리합니다.

Redis를 처음 만나면 보통 이렇게 기억한다.

메모리에 올려두는 빠른 cache.

틀린 말은 아니다. 하지만 Redis를 cache로만 보면 중요한 부분을 많이 놓친다. Redis는 단순 key-value cache라기보다, 메모리 위에서 동작하는 data structure server에 가깝다. 문자열 하나를 저장하는 것뿐 아니라 Hash, List, Set, Sorted Set, Stream 같은 자료구조를 명령어 단위로 조작한다.

그래서 Redis를 잘 쓴다는 말은 두 가지를 같이 이해한다는 뜻이다.

데이터를 어떻게 빨리 읽을 것인가
데이터가 많아지고 장애가 날 때 어떻게 버틸 것인가

Redis는 자료구조 서버다

Redis의 기본 단위는 key다. 하지만 value는 단순 문자열만이 아니다.

자료구조 잘 맞는 용도 조심할 점
String cache value, counter, token, flag 큰 JSON을 통째로 저장하면 부분 갱신과 네트워크 비용이 커진다.
Hash 객체의 field 단위 저장, profile, 설정값 field가 지나치게 많으면 한 key 안의 큰 collection이 된다.
List 간단한 FIFO/LIFO queue, blocking pop ack/retry/consumer group이 필요하면 Stream이 낫다.
Set 중복 없는 membership, tag, unique user set 큰 set 연산은 single thread를 오래 붙잡을 수 있다.
Sorted Set ranking, score 기반 range query, delayed job score 갱신이 잦고 크기가 크면 메모리와 CPU를 같이 쓴다.
Stream append-only event log, consumer group trim, pending entries, retry 정책을 같이 설계해야 한다.

Redis가 빠른 이유는 대부분의 명령을 메모리에서 처리하고, event loop 중심으로 단순하게 실행하기 때문이다. 하지만 이 말은 반대로, 오래 걸리는 명령 하나가 다른 요청들을 밀어낼 수 있다는 뜻이기도 하다.

그래서 운영 Redis에서 제일 먼저 피해야 할 습관은 “큰 key를 만들고, 큰 명령을 아무렇지 않게 호출하는 것”이다.

위험한 냄새가 나는 명령:
KEYS *
FLUSHALL
FLUSHDB
SAVE
큰 collection에 대한 전체 조회
큰 key에 대한 DEL

공식 문서도 KEYS는 production에서 극도로 조심해야 하고, 일반 애플리케이션 코드에서는 SCAN이나 별도 index set을 고려하라고 설명한다.

TTL은 collection 내부가 아니라 key에 붙는다

Redis의 expire는 key 단위다.

SET user:1:name "dngur"
EXPIRE user:1:name 60

이 key는 60초 뒤 만료될 수 있다. 하지만 Hash의 field 하나, List의 item 하나, Set의 member 하나에 개별 TTL이 붙는 것은 아니다.

HSET user:1 name "dngur" age 20
EXPIRE user:1 60

이 경우 TTL은 user:1이라는 Hash key 전체에 적용된다. field별 TTL이 필요하다면 key를 더 잘게 나누거나, Sorted Set에 만료 timestamp를 score로 넣고 별도 정리 작업을 두는 식으로 설계를 바꿔야 한다.

TTL은 cache freshness를 다루는 도구이고, eviction은 memory pressure를 다루는 도구다. 둘은 비슷해 보이지만 목적이 다르다.

TTL:
  시간이 지나면 사라져도 되는 데이터

eviction:
  maxmemory를 넘었을 때 무엇을 버릴지 정하는 정책

maxmemory와 eviction policy

Redis가 maxmemory에 도달하면, 설정된 maxmemory-policy에 따라 key를 제거하거나 쓰기를 거부한다.

순수 cache라면 allkeys-lruallkeys-lfu가 단순하다. TTL이 없는 cache key도 제거 후보가 되기 때문이다. 반대로 중요한 영구 key와 임시 key가 같은 Redis에 섞여 있다면 volatile-* 정책이 더 안전해 보일 수 있다. 다만 TTL 없는 key는 제거 후보가 아니므로, 메모리 압박이 왔을 때 정책이 기대대로 동작하는지 반드시 확인해야 한다.

noeviction은 key를 버리지 않는다. 대신 쓰기 명령이 에러를 반환할 수 있다. 캐시로 쓰는 Redis라면 장애 모드가 더 거칠어질 수 있고, source of truth처럼 쓰는 Redis라면 오히려 명시적 실패가 더 안전할 수 있다.

여기서 중요한 운영 감각이 하나 있다.

Redis eviction은 정확한 전역 LRU/LFU가 아니라 샘플링 기반 근사 알고리즘이다.

그래서 maxmemory-samples를 키우면 더 정확한 후보를 고를 수 있지만, 그만큼 CPU 비용도 늘어난다.

Persistence는 cache인지 data인지 먼저 정해야 한다

Redis는 in-memory로 동작하지만 디스크 persistence 옵션을 갖고 있다.

RDB = 특정 시점 snapshot
AOF = write command log
No persistence = 재시작하면 사라져도 되는 cache
RDB + AOF = 둘을 함께 사용

RDB는 point-in-time snapshot이다. 파일이 작고 재시작이 빠른 편이지만, snapshot 사이에 장애가 나면 최근 데이터가 사라질 수 있다. AOF는 write operation을 log로 남기고 재시작 시 replay한다. 더 촘촘한 복구가 가능하지만 파일 크기, fsync 정책, rewrite 비용을 같이 봐야 한다.

Redis를 cache로만 쓴다면 persistence를 끄는 선택도 자연스럽다. 재시작 후 cache miss가 늘 뿐, 원본 DB에서 다시 채울 수 있기 때문이다.

반대로 session, idempotency key, rate limit counter, queue처럼 Redis 안의 데이터가 서비스 의미를 갖는다면 persistence와 replication을 반드시 같이 봐야 한다. “Redis가 빠르다”와 “Redis에 저장한 데이터가 반드시 안전하다”는 다른 문장이다.

Sentinel과 Cluster는 해결하는 문제가 다르다

Sentinel과 Cluster는 둘 다 고가용성과 관련이 있지만, 같은 기능이 아니다.

구분 Sentinel Cluster
주요 목적 Master 장애 감지와 failover Sharding과 failover
데이터 분산 기본적으로 하나의 master dataset 16384 hash slot으로 keyspace 분산
클라이언트 요구 Sentinel을 통해 현재 master 발견 Cluster-aware client가 slot redirect를 처리해야 함
장애 판단 SDOWN, ODOWN, quorum 개념 노드 간 gossip, majority, replica promotion
주의점 scale-out이 아니라 failover 중심 multi-key command는 같은 slot 조건을 고려해야 함

Sentinel은 master를 감시하고, 충분한 Sentinel이 장애를 인정하면 replica 중 하나를 master로 승격한다. Redis 문서의 표현을 빌리면 SDOWN은 특정 Sentinel이 주관적으로 판단한 down 상태이고, ODOWN은 quorum 이상이 동의한 객관적 down 상태다.

Cluster는 keyspace를 16384개 hash slot으로 나눈다.

HASH_SLOT = CRC16(key) mod 16384

각 master node는 slot의 일부를 담당한다. client가 잘못된 node에 요청하면 Redis는 올바른 node를 알려주는 redirect를 반환하고, cluster-aware client는 이를 따라가야 한다.

Cluster에서는 key를 묶어 같은 slot으로 보내야 할 때 hash tag를 쓴다.

cart:{user-1}
cart-item:{user-1}:a
cart-item:{user-1}:b

중괄호 안의 user-1만 hash slot 계산에 쓰이므로, 관련 key를 같은 slot에 둘 수 있다. multi-key command나 transaction-like 흐름을 Redis Cluster에서 쓸 때 자주 필요한 감각이다.

Redis를 queue로 쓸 때는 보장 수준을 골라야 한다

Redis로 event queue를 만들 수 있다. 하지만 어떤 자료구조를 쓰는지에 따라 보장 수준이 완전히 달라진다.

Pub/Sub은 가장 가볍다. 하지만 Redis 공식 문서 기준으로 Pub/Sub은 at-most-once delivery semantics를 갖는다. subscriber가 연결되어 있지 않거나 처리 중 실패하면 메시지를 다시 받을 방법이 없다. 모니터링 알림, live notification처럼 유실이 치명적이지 않은 곳에 어울린다.

List는 간단한 작업 큐를 만들기 쉽다.

producer:
  LPUSH jobs payload

consumer:
  BRPOP jobs 5

여기서 BLPOP/BRPOP의 timeout 0은 즉시 반환이 아니라 무기한 대기다. 일정 시간마다 loop를 돌며 shutdown signal이나 health 상태를 확인하고 싶다면 0보다 작은 적절한 timeout을 두는 편이 운영하기 쉽다.

List는 “한 메시지를 한 consumer가 가져간다”는 queue에는 충분할 수 있다. 하지만 소비 후 ack, 실패 시 재처리, pending 상태 추적이 필요하면 직접 구현할 것이 많아진다.

Stream은 이 빈틈을 많이 채운다.

XADD order-events * orderId 123 status paid
XGROUP CREATE order-events order-workers $
XREADGROUP GROUP order-workers worker-1 COUNT 10 BLOCK 1000 STREAMS order-events >
XACK order-events order-workers 1680000000000-0

Stream은 append-only log에 가깝고, consumer group을 통해 여러 consumer가 같은 stream을 나누어 읽을 수 있다. XACK로 처리 완료를 표시하고, pending entries를 추적할 수 있다. 대신 stream이 무한히 커지지 않도록 XTRIM이나 maxlen 정책을 함께 설계해야 한다.

Spring Boot에서 Redis를 붙이는 기본 흐름

Spring Boot에서는 보통 spring-boot-starter-data-redis를 추가하면 Spring Data Redis가 Redis 접근 추상화를 제공한다. 기본 client는 Lettuce가 흔히 쓰인다.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
    implementation("org.apache.commons:commons-pool2")
}

가장 단순한 설정은 property로 시작한다.

spring:
  data:
    redis:
      host: localhost
      port: 6379
      connect-timeout: 500ms
      timeout: 1s
      lettuce:
        pool:
          max-active: 16
          max-idle: 8
          min-idle: 0
          max-wait: 500ms

StringRedisTemplate은 문자열 중심으로 쓸 때 편하다.

@Service
public class LoginAttemptStore {

    private final StringRedisTemplate redis;

    public LoginAttemptStore(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public long increase(String userId) {
        String key = "login:fail:" + userId;
        Long count = redis.opsForValue().increment(key);
        redis.expire(key, Duration.ofMinutes(10));
        return count == null ? 0 : count;
    }
}

객체를 저장한다면 serializer를 명시하는 편이 안전하다.

@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.afterPropertiesSet();
    return template;
}

serializer를 암묵적으로 두면 나중에 타입 변경, class package 변경, 다국어 client 접근, 운영 redis-cli 확인에서 비용이 커진다. key는 읽을 수 있는 문자열로 두고, value는 JSON인지 binary인지 명확히 정해두는 편이 좋다.

Spring Cache로 쓸 때

Spring Cache를 Redis 위에 올리면 @Cacheable로 cache-aside 패턴을 쉽게 만들 수 있다.

@Service
public class ProductService {

    @Cacheable(cacheNames = "product", key = "#id")
    public Product getProduct(Long id) {
        return productRepository.findById(id)
            .orElseThrow();
    }
}

CacheManager에는 TTL과 serializer를 같이 넣는다.

@Bean
RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(10))
        .disableCachingNullValues()
        .serializeKeysWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
        )
        .serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
        );

    return RedisCacheManager.builder(connectionFactory)
        .cacheDefaults(config)
        .withCacheConfiguration(
            "product",
            config.entryTtl(Duration.ofMinutes(5))
        )
        .build();
}

운영 cache에서 TTL은 거의 필수다. 영구 cache는 언젠가 source of truth와 어긋난다. TTL을 너무 길게 잡으면 stale data가 오래 남고, 너무 짧게 잡으면 DB를 보호하지 못한다.

여기에 cache stampede도 고려해야 한다. 같은 key가 동시에 만료되면 많은 요청이 한꺼번에 DB로 몰릴 수 있다. 해결책은 상황에 따라 다르다.

  • TTL에 작은 jitter를 섞는다.
  • hot key는 refresh-ahead로 미리 갱신한다.
  • miss 시 분산 lock이나 single-flight로 DB 조회를 합친다.
  • null caching을 쓸지 명확히 정한다.

Lettuce를 이해해야 하는 이유

Lettuce는 Netty 기반 Redis client다. Spring Data Redis는 Lettuce를 통해 non-blocking I/O 기반 client를 제공하지만, Spring MVC에서 RedisTemplate을 동기 방식으로 쓰면 애플리케이션 코드 관점에서는 동기 호출처럼 느껴진다.

중요한 지점은 connection이다.

Spring Data Redis의 LettuceConnectionFactory는 기본적으로 여러 LettuceConnection이 하나의 thread-safe native connection을 공유할 수 있다. 하지만 LettuceConnection 자체와 clustered variant는 thread-safe가 아니므로, 인스턴스를 여러 스레드에서 직접 공유하면 안 된다. 보통은 RedisTemplate이 connection 획득과 반환을 관리하므로 직접 만질 일이 적다.

공식 API 문서 기준으로 shareNativeConnectiontrue이면 일반 작업은 shared native connection을 사용하고, blocking이나 transaction 작업은 별도 connection provider를 통해 connection을 고른다. shareNativeConnectionfalse로 끄면 모든 작업이 새 connection 또는 pool connection을 사용한다.

shareNativeConnection과 connection pool의 관계

Spring Boot에서 Lettuce pool 설정을 넣으면 모든 Redis 명령이 pool에서 connection을 빌려 쓸 것처럼 느껴지지만, 기본 동작은 조금 다르다. 핵심은 LettuceConnectionFactoryshareNativeConnection 값이다.

기본값은 true다. 이때 RedisTemplateGET, SET, HGET, INCR 같은 일반 명령을 실행하면 매번 새로운 TCP connection을 빌리는 것이 아니라, factory 내부의 shared native connection을 함께 사용한다. Lettuce의 native connection인 StatefulRedisConnection은 thread-safe하고 command multiplexing을 지원하므로, 여러 application thread가 같은 native connection을 통해 명령을 보낼 수 있다.

다만 Spring Data Redis의 LettuceConnection 객체 자체는 thread-safe하지 않다. 그래서 LettuceConnection 인스턴스를 직접 필드에 저장해 여러 스레드에서 공유하는 방식은 피해야 한다. 일반적인 RedisTemplate 사용에서는 template이 connection 획득과 반환을 감싸기 때문에 이 차이를 직접 의식할 일이 적다.

정리하면 다음처럼 보면 된다.

설정 일반 명령 Blocking / transaction 의미
shareNativeConnection=true, pool 없음 shared native connection 별도 provider connection 대부분의 일반 cache 작업에 충분한 기본 모델
shareNativeConnection=true, pool 있음 shared native connection pool connection pool 설정을 해도 일반 GET/SET이 곧바로 pool을 타지는 않음
shareNativeConnection=false, pool 있음 pool connection pool connection 모든 작업이 pool에서 connection을 빌리는 방식
shareNativeConnection=false, pool 없음 새 connection 또는 전용 connection 새 connection 또는 전용 connection connection 생성 비용이 커질 수 있어 보통 권장하기 어렵다

따라서 spring.data.redis.lettuce.pool.* 값을 설정했다고 해서 애플리케이션의 모든 Redis 요청이 max-active 개수만큼 분산되는 것은 아니다. shareNativeConnection=true라면 CLIENT LIST에서 일반 명령용 shared connection이 중심으로 보이고, blocking이나 transaction을 사용할 때 pool connection이 추가로 보이는 식으로 이해하는 편이 맞다.

setShareNativeConnection(false)는 이런 기본 공유 모델을 끄는 설정이다. 예를 들어 모든 동기 Redis 작업을 pool에서 빌린 connection으로 격리하고 싶다면 factory를 직접 만들고 값을 꺼야 한다.

@Bean
LettuceConnectionFactory pooledDedicatedRedisConnectionFactory() {
    RedisStandaloneConfiguration server = new RedisStandaloneConfiguration("localhost", 6379);

    GenericObjectPoolConfig<?> pool = new GenericObjectPoolConfig<>();
    pool.setMaxTotal(16);
    pool.setMaxIdle(8);
    pool.setMinIdle(0);
    pool.setMaxWait(Duration.ofMillis(500));

    LettuceClientConfiguration client = LettucePoolingClientConfiguration.builder()
        .poolConfig(pool)
        .commandTimeout(Duration.ofSeconds(1))
        .build();

    LettuceConnectionFactory factory = new LettuceConnectionFactory(server, client);
    factory.setShareNativeConnection(false);
    return factory;
}

하지만 이 설정을 성능 향상 버튼처럼 생각하면 안 된다. Redis server는 기본적으로 명령을 빠르게 순차 처리하고, Lettuce connection은 thread-safe하게 multiplexing할 수 있다. 일반적인 cache read/write에서는 connection을 늘리는 것보다 key 설계, command latency, payload 크기, slow command 제거, timeout 설정이 더 큰 영향을 준다.

shareNativeConnection=false가 의미 있는 경우는 보통 다음 쪽에 가깝다.

  • connection 단위 상태가 섞이면 안 되는 작업을 명확히 격리하고 싶다.
  • transaction, blocking command, batch성 작업이 섞여 일반 cache 요청과 분리하고 싶다.
  • shared connection에서 특정 장애나 지연이 전체 요청에 영향을 주는지 검증하기 위해 실험적으로 비교하고 싶다.
  • 운영 정책상 connection 수, pool wait, pool exhaustion을 명시적으로 관찰하고 제어하고 싶다.

반대로 단순히 “Redis가 느리니 pool을 늘리자”는 접근은 위험하다. Redis slowlog에 비싼 명령이 찍히거나, network latency가 튀거나, 큰 value 직렬화가 병목인 상황에서는 pool을 늘려도 병목 위치가 바뀌지 않는다. pool을 키우면 Redis server 입장에서는 더 많은 client connection과 더 많은 동시 대기열을 감당해야 하므로 장애가 더 늦게 드러날 수도 있다.

shared native connection은 LettuceConnection이 닫는 대상이 아니기 때문에, getConnection() 때마다 기본으로 검증되지 않는다. 연결 검증이 꼭 필요하면 setValidateConnection(true)를 켤 수 있지만, 검증 비용이 추가될 수 있으므로 장애 증상과 운영 환경을 보고 선택해야 한다.

Lettuce 설정 예시

기본 property로 충분하지 않다면 LettuceConnectionFactory를 명시적으로 구성한다.

@Configuration
public class RedisConfig {

    @Bean
    LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration server = new RedisStandaloneConfiguration("localhost", 6379);

        LettuceClientConfiguration client = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(1))
            .shutdownTimeout(Duration.ofMillis(100))
            .clientOptions(ClientOptions.builder()
                .autoReconnect(true)
                .build())
            .build();

        return new LettuceConnectionFactory(server, client);
    }
}

pool이 필요하면 commons-pool2와 함께 pooling configuration을 쓴다.

@Bean
LettuceConnectionFactory pooledRedisConnectionFactory() {
    RedisStandaloneConfiguration server = new RedisStandaloneConfiguration("localhost", 6379);

    GenericObjectPoolConfig<?> pool = new GenericObjectPoolConfig<>();
    pool.setMaxTotal(16);
    pool.setMaxIdle(8);
    pool.setMinIdle(0);
    pool.setMaxWait(Duration.ofMillis(500));

    LettuceClientConfiguration client = LettucePoolingClientConfiguration.builder()
        .poolConfig(pool)
        .commandTimeout(Duration.ofSeconds(1))
        .build();

    return new LettuceConnectionFactory(server, client);
}

pool을 늘린다고 Redis 처리량이 무조건 늘지는 않는다. Redis server는 명령을 매우 빠르게 처리하지만, single-thread event loop 특성상 비싼 명령이 섞이면 queueing이 생긴다. connection pool은 client 쪽 대기와 격리를 도와줄 수 있지만, Redis server의 CPU, command latency, network, slowlog가 병목이면 pool만 늘려서는 해결되지 않는다.

Lettuce 사용 시 유의사항

1. timeout은 짧고 명확하게 둔다

Redis는 빠른 저장소로 쓰는 경우가 많다. 그러면 timeout도 그 기대에 맞아야 한다. 요청 전체 SLA가 200ms인데 Redis command timeout이 60초라면, 장애 시 애플리케이션 thread가 너무 오래 붙잡힌다.

connect timeout:
  Redis에 새 연결을 맺는 시간

command timeout:
  GET, SET 같은 명령이 끝나길 기다리는 시간

pool max-wait:
  pool에서 connection을 빌릴 때 기다리는 시간

이 셋은 서로 다르다. timeout 로그를 볼 때도 “connect가 느린지, pool이 고갈됐는지, Redis command가 느린지”를 분리해서 봐야 한다.

2. blocking command는 connection을 분리한다

BLPOP, BRPOP, Pub/Sub subscribe 같은 작업은 connection을 오래 점유한다. 일반 cache GET/SET과 같은 connection을 공유하면 다른 명령이 밀릴 수 있다.

Spring Data Redis의 shareNativeConnection 기본 동작은 일반 작업과 blocking/transaction 작업을 구분하려고 한다. 하지만 직접 connection을 다루거나, custom factory/pool을 구성한다면 blocking 작업 전용 connection 또는 별도 RedisTemplate을 두는 편이 명확하다.

3. 큰 key 삭제는 UNLINK를 고려한다

큰 List, Set, Hash를 DEL로 지우면 메모리 해제가 main thread에서 부담이 될 수 있다. Redis에는 key를 keyspace에서 분리하고 실제 메모리 회수를 background에서 처리하는 UNLINK가 있다. 큰 key 정리 작업이라면 DELUNLINK의 차이를 이해하고 선택해야 한다.

4. Cluster에서는 key naming이 routing이다

Redis Cluster에서는 key가 hash slot을 결정한다. 관련 key를 같은 slot에 두어야 한다면 hash tag를 써야 한다.

order:{123}:summary
order:{123}:items
order:{123}:payment

이렇게 하면 {123} 부분만 hash slot 계산에 쓰여 같은 slot으로 간다. 반대로 아무 생각 없이 key를 만들면 multi-key command가 cluster에서 실패하거나 redirect가 늘어날 수 있다.

5. cache miss도 장애 모드다

Redis cache 장애는 Redis가 죽었을 때만 생기지 않는다. eviction, TTL 만료, deploy 후 cold cache, hot key 만료가 모두 DB로 전이될 수 있다.

Redis hit:
  빠른 응답

Redis miss:
  DB query
  serialization
  Redis write-back
  동시 요청이면 stampede 가능

Redis를 붙였으면 hit ratio, command latency, used memory, evicted keys, expired keys, connected clients, slowlog, replication lag를 같이 봐야 한다.

운영 체크리스트

Redis를 붙이기 전에 다음 질문을 먼저 정리해두면 장애 때 덜 흔들린다.

  • 이 Redis는 cache인가, session store인가, queue인가, source of truth에 가까운가?
  • key마다 TTL이 있는가?
  • maxmemorymaxmemory-policy는 무엇인가?
  • eviction이 발생해도 서비스 의미가 깨지지 않는가?
  • RDB/AOF persistence를 켤 것인가?
  • Sentinel/Cluster 중 어떤 topology가 필요한가?
  • 큰 collection key가 생기지 않는가?
  • KEYS, 큰 DEL, FLUSH*, SAVE 같은 명령이 운영 경로에 없는가?
  • Spring Boot에서 command timeout, connect timeout, pool wait timeout이 분리되어 있는가?
  • blocking Redis 작업이 일반 cache 작업과 connection을 공유하지 않는가?
  • serializer와 key naming 규칙이 문서화되어 있는가?

한 문장으로 정리하면

Redis는 빠른 cache이기도 하지만, 실제로는 TTL, eviction, persistence, topology, client connection까지 함께 설계해야 하는 in-memory data structure server다.

Spring Boot에서 Lettuce를 쓸 때도 핵심은 같다. RedisTemplate은 Redis를 쉽게 쓰게 해주지만, timeout과 connection 공유, blocking command, serializer, key naming을 흐릿하게 만들면 장애는 훨씬 늦게 드러난다.

Redis를 잘 쓰는 기준은 “빨랐다”가 아니라 “느려지고, 메모리가 차고, master가 바뀌고, key가 사라져도 어떤 일이 일어나는지 알고 있다”에 가깝다.

참고한 자료


© 2024. All rights reserved.