Redis 자료구조별 캐싱 패턴 — 만료 단위·축출 정책 (Redis 8)

데이터베이스 2026년 8월 7일  ·  2026. 10. 30 업데이트  ·  junetapa  ·  약 8분

Redis 자료구조 선택이 캐싱 성능을 가르는 지점

캐시를 도입할 때 가장 흔한 출발점은 "직렬화한 객체를 문자열로 넣고 TTL을 건다"이다. 동작은 하지만, Redis가 제공하는 자료구조를 String 하나로만 쓰는 순간 부분 갱신·순위 조회·중복 제거 같은 작업이 전부 애플리케이션 쪽으로 밀려난다. 자료구조 선택은 편의의 문제가 아니라 명령 복잡도·메모리 인코딩·만료 단위가 함께 결정되는 문제다.

단일 스레드 실행 모델과 명령 복잡도

Redis는 명령 실행을 단일 스레드로 처리한다(I/O 스레드와 백그라운드 해제 작업은 별개다). 따라서 O(N) 명령 하나가 그 시간 동안 다른 모든 클라이언트를 대기시킨다. 공식 문서의 각 명령 페이지에는 시간 복잡도가 명시되어 있는데, 캐싱 설계에서 특히 주의할 것은 다음과 같다.

내부 인코딩과 메모리 임계값

같은 자료구조라도 크기에 따라 내부 표현이 달라진다. 최신 버전 기준으로 작은 Hash·List·Sorted Set은 listpack(과거 ziplist)으로, 정수만 담긴 작은 Set은 intset으로 압축 저장된다. 설정 기본값은 대체로 hash-max-listpack-entries 128 / hash-max-listpack-value 64, zset-max-listpack-entries 128 / zset-max-listpack-value 64, set-max-intset-entries 512 수준이다(버전별로 항목명이 ziplist에서 listpack으로 바뀌었으므로 사용 중인 버전의 redis.conf를 확인해야 한다).

중요한 성질은 변환이 단방향이라는 점이다. 임계값을 넘어 hashtable이나 skiplist로 승격된 키는 이후 요소가 줄어도 자동으로 압축 표현으로 되돌아가지 않는다. "작은 해시 여러 개"와 "큰 해시 하나"의 메모리 사용량이 크게 갈리는 이유가 여기 있다.

512MB 상한과 빅키

String 값의 최대 크기는 512MB이고, Bitmap은 String 위에 구현되므로 최대 2^32비트라는 한계를 공유한다. 상한 자체보다 실무적으로 먼저 문제가 되는 것은 빅키다. 수십 MB짜리 값 하나를 읽고 쓰는 동안 서버가 점유되고, 클러스터에서는 슬롯 마이그레이션과 복제 지연까지 영향을 받는다.

자료구조별 캐싱 패턴

String과 Hash — 객체 캐시의 두 갈래

String은 직렬화된 페이로드를 통째로 캐싱할 때 가장 단순하다. SET key value EX 300 NX처럼 만료와 조건부 쓰기를 한 명령으로 처리할 수 있고, GETEX(TTL 갱신 조회), GETDEL(조회 후 삭제)은 Redis 6.2에서 추가됐다. 카운터 용도로는 INCR·INCRBY가 원자적으로 동작해 애플리케이션 락이 필요 없다.

스트링과 해시, 리스트, 셋, 소트드셋 등 레디스 자료구조별 캐싱 쓰임을 정리한 도해
통째로 담을지 부분만 고칠지가 자료구조를 정한다.

Hash는 객체의 필드를 개별 주소로 노출한다. 프로필 중 닉네임만 바뀌었을 때 String이라면 전체를 다시 직렬화해 덮어써야 하지만, Hash라면 HSET user:1 nickname ... 한 번이면 된다. 다만 TTL 단위가 오랫동안 약점이었다. 전통적으로 만료는 키 단위였고, 필드별 만료는 Redis 7.4에서 HEXPIRE·HPEXPIRE·HTTL·HPERSIST가 도입되면서 지원됐다. 7.4 미만을 쓴다면 "필드마다 수명이 다른 데이터"는 Hash가 아니라 개별 String 키로 쪼개는 편이 맞다. Redis 8.0부터는 값 쓰기와 필드 만료를 한 명령으로 묶는 HSETEX, 읽으면서 만료를 다시 거는 HGETEX가 공식 명령 문서에 올라 있다(둘 다 «since 8.0.0»). 예를 들어 HSETEX session:1 EX 1800 FIELDS 2 token abc csrf xyz는 두 필드를 쓰면서 30분 만료를 함께 건다. FNX(모든 필드가 없을 때만)·FXX(모든 필드가 있을 때만)·KEEPTTL(기존 만료 유지) 조건도 붙일 수 있다.

List·Set·Sorted Set — 순서와 순위

List는 양끝 삽입·삭제가 O(1)이라 최근 항목 캐시에 적합하다. LPUSH 후 LTRIM key 0 99로 길이를 고정하는 것이 관용적인 상한 유지 방식이다. BLPOP·BRPOP은 블로킹 소비를 제공하지만, 메시지가 소비되면 사라지고 재처리 보장이 없으므로 큐로 쓸 때는 뒤에 나올 Stream과 비교해야 한다.

Set은 중복 제거와 소속 검사가 목적이다. SISMEMBER는 O(1)이고, Redis 6.2에서 여러 멤버를 한 번에 검사하는 SMISMEMBER가 추가됐다. 교집합 크기만 필요할 때는 결과 집합을 만들지 않는 SINTERCARD(7.0)가 SINTER보다 저렴하다.

Sorted Set은 점수 기반 정렬을 유지한다. 삽입·삭제·순위 조회가 O(log N)이며, 랭킹 캐시뿐 아니라 타임스탬프를 점수로 쓰는 구간 조회, 정렬 순서를 이용한 지연 큐, 슬라이딩 윈도우 레이트 리미터에도 쓰인다. ZADD의 GT·LT 옵션(6.2)은 "기존보다 큰 점수일 때만 갱신" 같은 조건을 서버에서 처리해 준다.

Bitmap·HyperLogLog·Stream — 집계와 이벤트

Bitmap은 사용자 ID를 비트 오프셋으로 매핑해 출석·플래그를 저장한다. 조밀한 ID 공간에서는 메모리 효율이 압도적이지만, ID가 희소하면 오프셋 최대값만큼의 공간이 잡히므로 오히려 낭비다.

HyperLogLog는 고유 개수 추정 전용이다. 공식 문서 기준 표준 오차는 0.81%, 키 하나의 크기는 최대 12KB로 고정된다. 정확한 개수나 개별 원소 조회가 필요하면 Set을, 오차를 감수하고 상수 메모리를 얻고 싶으면 HyperLogLog를 쓴다.

Stream은 추가 전용 로그에 소비자 그룹, 확인 응답(XACK), 미확인 목록(XPENDING), 소유권 이전(XAUTOCLAIM, 6.2)을 얹은 구조다. 재처리와 다중 소비자가 필요한 이벤트 파이프라인이라면 List 대신 Stream이 정답에 가깝다. 무한 증가를 막으려면 XADD ... MAXLEN ~ 10000 같은 트리밍을 함께 건다.

비교표와 선택 기준

자료구조별 비교

자료구조 대표 캐싱 용도 핵심 복잡도 주의할 제약
String 직렬화 객체, 렌더링 결과, 카운터 GET/SET O(1) 값 최대 512MB, 부분 갱신 불가
Hash 필드 단위 객체 캐시, 설정값 HGET O(1), HGETALL O(N) 필드별 TTL은 7.4 이상, listpack 임계값 초과 시 메모리 증가
List 최근 목록, 단순 작업 큐 LPUSH/RPOP O(1), LRANGE O(N) 소비 후 소실, 재처리 보장 없음
Set 중복 제거, 태그, 권한 검사 SADD/SISMEMBER O(1) SMEMBERS는 O(N), 순서 없음
Sorted Set 랭킹, 시간 구간 조회, 레이트 리밋 ZADD/ZRANK O(log N) 동일 점수 다수면 사전순 정렬에 의존
Bitmap 출석·플래그 집계 SETBIT O(1), BITCOUNT O(N) ID가 희소하면 공간 낭비, 2^32비트 상한
HyperLogLog 고유 방문자 추정 PFADD O(1) 표준 오차 0.81%, 개별 원소 조회 불가
Stream 이벤트 로그, 소비자 그룹 큐 XADD O(1), 범위 조회 O(N) 트리밍 없으면 무한 증가

반드시 확인할 제약 세 가지

만료와 무효화, 그리고 캐싱 패턴의 조합

읽기·쓰기 경로 패턴

자료구조를 정했다면 다음은 원본 데이터 저장소와의 관계다. 널리 쓰이는 캐싱 패턴은 세 갈래로 정리된다. Cache-Aside(지연 로딩)는 애플리케이션이 캐시를 먼저 조회하고 미스일 때 DB에서 읽어 채운다. 구현이 단순하고 캐시 장애가 곧 서비스 장애가 되지 않는 대신, 첫 요청은 항상 미스이고 데이터가 낡을 수 있다. Write-Through는 쓰기 시점에 캐시와 DB를 함께 갱신해 신선도를 확보하지만 쓰기 지연이 늘고, 읽히지 않을 데이터까지 캐시에 채운다. Write-Behind는 캐시에 먼저 쓰고 비동기로 DB에 반영해 쓰기 처리량을 얻지만 유실 위험을 감수한다. 실제로는 Cache-Aside를 기본으로 두고, 신선도가 중요한 일부 키에만 Write-Through를 얹는 조합이 흔하다.

만료와 무효화, 갱신으로 이어지는 캐시 일관성 전략을 나타낸 도해
캐시의 어려움은 채우는 것이 아니라 «버리는 것»에 있다.

만료 처리 방식과 무효화

Redis의 키 만료는 두 방식이 함께 동작한다. 하나는 해당 키에 접근할 때 만료 여부를 확인해 제거하는 수동 방식이고, 다른 하나는 만료 시각이 설정된 키를 일정 간격으로 표본 추출해 정리하는 능동 방식이다. 즉 TTL이 지났다고 즉시 메모리가 반환되지는 않는다. 만료 시점에 후속 처리가 필요하면 키스페이스 알림(notify-keyspace-events)을 켜서 이벤트를 받을 수 있지만, 이 알림은 최선 노력 전달이므로 정확성이 필요한 로직의 유일한 근거로 삼으면 안 된다. 애플리케이션 로컬 캐시를 함께 쓴다면 Redis 6.0의 클라이언트 사이드 캐싱(RESP3의 CLIENT TRACKING)이 무효화 통지를 서버가 밀어 주는 방식으로 지원한다.

캐시 스탬피드 방어

같은 시각에 만료된 인기 키에 요청이 몰리면 전부 미스가 나면서 원본 DB로 동시에 쏟아진다. 완화 수단은 세 가지다. 첫째, TTL에 무작위 지터를 더해 만료 시점을 분산한다. 둘째, SET lock:key token NX EX 10 형태의 조건부 쓰기로 재계산 권한을 하나의 요청에만 부여하고 나머지는 이전 값을 반환하거나 짧게 대기한다. 셋째, 논리적 만료를 써서 값 안에 만료 시각을 함께 저장하고, 실제 키는 더 길게 유지하되 만료가 임박하면 백그라운드에서 갱신한다. 잠금을 쓸 때는 해제 시 자신이 건 잠금인지 토큰을 대조해야 하며, 이 비교와 삭제는 Lua 스크립트로 원자적으로 처리하는 것이 안전하다.

어떤 경우에 무엇이 적합한가

조건별 정리

도입 전 점검 목록

정리하면 Redis 캐싱 패턴 설계는 세 가지 질문으로 수렴한다. 갱신과 만료의 최소 단위는 무엇인가(키인가 필드인가), 이 컬렉션의 요소 수 상한을 무엇으로 강제하는가, 메모리가 한계에 닿았을 때 어떤 키가 어떤 정책으로 사라지는가. 여기에 클러스터를 쓴다면 다중 키 연산이 같은 슬롯에 놓이는지를 더한다. 이 네 가지가 정해지면 자료구조는 대체로 하나로 좁혀진다.

마지막으로, 각 명령의 시간 복잡도·도입 버전·설정 기본값은 Redis 버전에 따라 달라진다. listpack 관련 설정명이 ziplist에서 바뀐 것, Hash 필드 TTL이 7.4에서 추가된 것처럼 버전 경계가 실제 설계 선택을 바꾸는 사례가 적지 않으므로, 운영에 반영하기 전에 사용 중인 버전의 공식 명령 레퍼런스와 redis.conf 기본값을 직접 대조하는 편이 안전하다.

메모리가 찼을 때 — maxmemory와 축출 정책

캐시 설계의 마지막 질문은 «메모리가 한계에 닿으면 무엇을 버리는가»다. Redis 공식 축출(eviction) 문서에 따르면 maxmemory를 0으로 두면 메모리 상한이 없고, 이것이 64비트 시스템의 기본값이다. 캐시로 쓴다면 상한을 정하고 maxmemory-policy로 버릴 키를 고르는 규칙을 정해야 한다. 복제나 AOF를 쓰는 서버는 그 버퍼 메모리가 상한 계산에 들어가지 않으므로, 문서는 버퍼 몫을 남기고 상한을 잡으라고 권한다. INFO memory의 mem_not_counted_for_evict 값이 그 크기를 어림하는 데 쓰인다.

정책버리는 키어울리는 경우
noeviction버리지 않고 새 데이터 쓰기를 오류로 막음캐시가 아니라 원본 저장소로 쓸 때
allkeys-lru전체 키 중 가장 오래 안 쓴 키일부 키에 접근이 몰릴 때 · 문서가 «특별한 이유가 없을 때 좋은 기본값»이라고 적음
allkeys-lrm전체 키 중 가장 오래 안 고친 키(8.6부터)읽기는 많지만 갱신이 끊긴 데이터를 먼저 버리고 싶을 때
allkeys-lfu전체 키 중 가장 드물게 쓴 키(4.0부터)오래 꾸준히 인기 있는 키를 지키고 싶을 때
allkeys-random무작위모든 키를 고르게 읽을 때
volatile-*만료가 걸린 키 안에서만 위 방식으로한 인스턴스에 캐시와 영구 키를 섞을 때(문서는 가능하면 인스턴스를 나누라고 권함)
volatile-ttl남은 TTL이 가장 짧은 키코드가 버릴 후보에 짧은 TTL을 직접 줄 수 있을 때

주의할 점이 둘 있다. volatile-* 정책은 만료가 걸린 키가 하나도 없으면 noeviction처럼 동작해 쓰기가 막힌다. 그리고 LRU·LFU·LRM 모두 정확한 계산이 아니라 키 몇 개를 표본으로 뽑아 고르는 근사 방식이며, 표본 수는 maxmemory-samples(기본 5)로 조절한다. LRM은 LRU와 같지만 읽기에는 시각을 갱신하지 않고 쓰기에만 갱신한다는 점이 다르다.

정책을 정한 뒤에는 숫자로 확인한다. 문서가 제시하는 적중률은 INFO stats의 keyspace_hits / (keyspace_hits + keyspace_misses)다. 적중률이 기대보다 낮으면 evicted_keys가 많은지(정책이 엉뚱한 키를 버림), expired_keys가 많은지(TTL이 너무 짧음)를 나눠 본다. 캐시 지표를 대시보드와 알림으로 묶는 방법은 실시간 성능 모니터링과 자동 스케일링에서, 캐시 앞단의 원본 쿼리를 줄이는 방법은 데이터베이스 쿼리 최적화에서 다뤘다. 문서형 저장소에서 같은 «묶을까 나눌까» 고민은 NoSQL 데이터 모델링 패턴과 이어진다.

판 기준도 적어 둔다. 2026년 10월 30일 GitHub 릴리스 기준 Redis의 최신 판은 8.10.3(10월 26일)이고, Redis에서 갈라져 나온 Valkey는 9.1.3(10월 26일)이다. 이 글의 명령 · 정책 설명은 Redis 공식 문서 기준이므로, Valkey나 관리형 서비스를 쓴다면 같은 명령 · 정책이 있는지 그쪽 문서에서 다시 확인한다.

Redis 자료구조별 캐싱 패턴 대표 그림 — 왼쪽에 제목 글자가 있고, 오른쪽에 모양이 다른 데이터 블록들이 선반에 정리되고 시계와 휴지통이 옆에 놓인 장면
Redis 자료구조별 캐싱 패턴 — 만료 단위·축출 정책 (Redis 8).

자주 묻는 것

Hash의 필드마다 만료를 따로 걸 수 있나요?

Redis 7.4부터 HEXPIRE·HTTL 같은 필드 만료 명령이 있고, 8.0부터는 값을 쓰면서 필드 만료를 함께 거는 HSETEX와 읽으면서 만료를 거는 HGETEX가 있습니다. 그보다 낮은 판이라면 필드마다 수명이 다른 데이터는 개별 String 키로 나누는 편이 맞습니다.

캐시용 Redis에는 어떤 축출 정책이 기본으로 좋나요?

Redis 공식 문서는 일부 키에 접근이 몰리는 흔한 경우를 들어 allkeys-lru를 특별한 이유가 없을 때 좋은 기본값으로 꼽습니다. 읽기는 많지만 갱신이 멈춘 데이터를 먼저 버리고 싶다면 8.6부터 있는 allkeys-lrm도 후보입니다.

TTL이 지나면 메모리가 바로 돌아오나요?

바로 돌아온다고 볼 수 없습니다. 만료된 키는 접근할 때 지워지거나, 만료가 걸린 키를 표본으로 골라 정리하는 과정에서 지워집니다. 큰 컬렉션을 지울 때는 메모리 회수를 백그라운드로 넘기는 UNLINK를 씁니다.

캐시 스탬피드는 어떻게 막나요?

TTL에 무작위 지터를 더해 만료 시점을 흩고, SET NX EX로 재계산 잠금을 한 요청에만 주며, 값 안에 논리 만료 시각을 넣어 만료 전에 백그라운드에서 갱신하는 방법을 섞어 씁니다. 잠금 해제는 토큰을 비교해 Lua 스크립트로 원자적으로 처리합니다.

참고 자료

  1. Redis Docs. Key eviction — maxmemory 기본값, 축출 정책 목록과 고르는 기준, allkeys-lru 권고, LRM(8.6)·LFU(4.0), maxmemory-samples, INFO 적중률 계산(2026-10-30 확인). redis.io
  2. Redis Docs. HSETEX — since 8.0.0, FNX·FXX·KEEPTTL(2026-10-30 확인). redis.io
  3. Redis Docs. HGETEX — since 8.0.0, EX·PX·EXAT·PXAT·PERSIST(2026-10-30 확인). redis.io
  4. Redis Releases(GitHub) — 8.10.3(2026-10-26)(2026-10-30 확인). github.com
  5. Valkey Releases(GitHub) — 9.1.3(2026-10-26)(2026-10-30 확인). github.com
Redis 캐싱 패턴 자료구조 데이터베이스 junetapa
junetapa(박준철)
개발자가 아니라 쓰는 사람. IT 현장 27년의 기준으로 도구를 고르고 기록한다.
Share Twitter Facebook URL 복사