Redis 메모리 최적화가 중요한 이유
Redis는 모든 데이터를 메모리에 저장합니다. 메모리는 디스크보다 수십 배 비싸므로, 같은 데이터를 더 적은 메모리로 저장하는 것이 운영 비용에 직결됩니다. 100GB 데이터를 최적화하면 30~50GB까지 줄일 수 있습니다. 이 글에서는 Redis 내부 인코딩 메커니즘, 데이터 구조별 메모리 특성, 실전 최적화 패턴을 다룹니다.
내부 인코딩: ziplist vs hashtable
Redis는 같은 데이터 타입이라도 크기에 따라 내부 인코딩을 자동 전환합니다.
| 데이터 타입 | 소량 데이터 | 대량 데이터 | 전환 임계값 |
|---|---|---|---|
| Hash | listpack (ziplist) | hashtable | 128개 또는 64바이트 |
| List | listpack | quicklist | 128개 또는 64바이트 |
| Set | listpack | hashtable | 128개 또는 64바이트 |
| ZSet | listpack | skiplist + hashtable | 128개 또는 64바이트 |
| String (정수) | int | embstr/raw | long 범위 이내 |
# 인코딩 확인
> OBJECT ENCODING user:1001
"listpack" # 메모리 효율적
> HSET user:1001 field1 val1 field2 val2 ... (129개 필드 추가)
> OBJECT ENCODING user:1001
"hashtable" # 메모리 사용 증가
# 임계값 설정
hash-max-listpack-entries 128 # 필드 수 임계값
hash-max-listpack-value 64 # 값 크기 임계값 (바이트)
zset-max-listpack-entries 128
zset-max-listpack-value 64
listpack(ziplist)은 메모리를 최대 10배 절약합니다. 연속된 메모리 블록에 데이터를 압축 저장하므로 오버헤드가 극히 적습니다. 임계값을 넘으면 hashtable로 전환되어 메모리 사용이 급증합니다.
String vs Hash: 키 설계 전략
사용자 100만 명의 프로필을 저장하는 두 가지 방식을 비교합니다.
# 방법 1: 개별 String (비효율)
SET user:1001:name "홍길동"
SET user:1001:email "hong@example.com"
SET user:1001:age "30"
# 키 3백만 개 → 키 자체의 오버헤드가 막대
# 방법 2: Hash (권장)
HSET user:1001 name "홍길동" email "hong@example.com" age "30"
# 키 100만 개 → 필드가 listpack으로 압축
# 방법 3: Hash 버켓팅 (극한 최적화)
# user:1001 → 버켓 10 (1001 / 100), 필드 1001
HSET users:10 1001:name "홍길동" 1001:email "hong@example.com"
# 키 1만 개 → 각 Hash에 100명씩, 모두 listpack 유지
| 방법 | 키 수 (100만 유저) | 예상 메모리 |
|---|---|---|
| 개별 String | 300만 | ~450MB |
| Hash (유저별) | 100만 | ~180MB |
| Hash 버켓팅 | 1만 | ~60MB |
Hash 버켓팅은 메모리를 7배 이상 절약할 수 있지만, 개별 키 TTL이 불가능하고 코드 복잡도가 올라갑니다. 트레이드오프를 고려해서 선택하세요.
메모리 분석 도구
최적화 전에 현재 메모리 사용 현황을 분석해야 합니다.
# 1. 전체 메모리 통계
> INFO memory
used_memory_human: 2.5G
used_memory_rss_human: 3.1G # RSS (실제 물리 메모리)
mem_fragmentation_ratio: 1.24 # 단편화 비율 (1.0~1.5 정상)
used_memory_dataset_perc: 82.3% # 데이터가 차지하는 비율
# 2. 개별 키 메모리 확인
> MEMORY USAGE user:1001
(integer) 128 # 바이트
# 3. 큰 키 찾기
> redis-cli --bigkeys
# Sampled 1000000 keys
# Biggest hash found 'session:abc123' has 5000 fields
# Biggest zset found 'leaderboard' has 100000 members
# 4. 상세 분석 (redis-memory-analyzer)
> redis-cli --memkeys
# 키 패턴별 메모리 사용량 분석
mem_fragmentation_ratio가 1.5를 넘으면 메모리 단편화가 심각한 상태입니다. Redis 4.0+에서는 activedefrag yes로 자동 조각 모음을 활성화할 수 있습니다.
TTL과 Eviction 전략
메모리 한계에 도달했을 때의 동작을 제어합니다.
# maxmemory 설정
maxmemory 4gb
maxmemory-policy allkeys-lru # 전체 키 중 LRU 삭제
# Eviction 정책 비교
# noeviction: 메모리 초과 시 쓰기 거부 (기본값)
# allkeys-lru: 모든 키에서 LRU 삭제 (캐시 용도 권장)
# allkeys-lfu: 모든 키에서 LFU 삭제 (Redis 4.0+, 더 정확)
# volatile-lru: TTL 설정된 키에서 LRU 삭제
# volatile-lfu: TTL 설정된 키에서 LFU 삭제
# volatile-ttl: TTL 가장 짧은 키 먼저 삭제
# allkeys-random: 랜덤 삭제
# TTL 배치 설정 팁
# 같은 시간에 대량 만료 → thundering herd 방지
SET session:1001 data EX 3600 # 정확히 1시간
SET session:1002 data EX 3540 # 약간의 랜덤 지터 추가
# 프로그래밍으로 지터 추가
import random
base_ttl = 3600
jitter = random.randint(-300, 300) # ±5분
redis.setex(key, base_ttl + jitter, value)
LFU vs LRU: LRU는 최근 접근 시간만 보지만, LFU는 접근 빈도를 추적합니다. 자주 조회되는 인기 데이터가 있는 캐시에서는 allkeys-lfu가 히트율이 더 높습니다. Redis 캐시 전략: Cache-Aside와 결합하면 효과적인 캐시 레이어를 구축할 수 있습니다.
압축과 직렬화 최적화
# 1. 짧은 키 이름 사용
# BAD: user:profile:1001:email_address
# GOOD: u:p:1001:em
# 키 100만 개 × 20바이트 절약 = 20MB
# 2. 정수 활용
# BAD: SET status "active" → embstr 인코딩
# GOOD: SET status 1 → int 인코딩 (8바이트)
# 3. MessagePack / Protobuf 직렬화
import msgpack
# JSON: {"name":"홍길동","age":30,"email":"hong@ex.com"} → 52바이트
# MessagePack: 같은 데이터 → 34바이트 (35% 절약)
data = {"name": "홍길동", "age": 30, "email": "hong@ex.com"}
packed = msgpack.packb(data) # 바이너리 직렬화
redis.set("user:1001", packed)
user = msgpack.unpackb(redis.get("user:1001"))
JSON 대신 MessagePack이나 Protobuf를 사용하면 30~50% 메모리를 절약할 수 있습니다. 가독성은 떨어지지만 대규모 데이터에서는 비용 차이가 큽니다.
Lazy-free와 비동기 삭제
# 큰 키 삭제 시 블로킹 방지
lazyfree-lazy-eviction yes # eviction 시 비동기 삭제
lazyfree-lazy-expire yes # TTL 만료 시 비동기 삭제
lazyfree-lazy-server-del yes # RENAME 등 내부 삭제 시
lazyfree-lazy-user-del yes # DEL 명령을 UNLINK처럼 동작
# UNLINK: 비동기 삭제 (메인 스레드 블로킹 없음)
> UNLINK large-hash-key # 백그라운드에서 메모리 해제
# vs DEL: 동기 삭제 (100만 필드 Hash → 수 초 블로킹)
100만 개 이상의 필드를 가진 Hash를 DEL로 삭제하면 Redis가 수 초간 블로킹됩니다. UNLINK를 사용하거나 lazyfree 옵션을 활성화하세요. Redis Cluster 샤딩·페일오버 환경에서는 특히 중요합니다.
운영 체크리스트
| 항목 | 권장 설정 |
|---|---|
| listpack 임계값 | hash-max-listpack-entries 128 |
| Eviction | 캐시: allkeys-lfu, 세션: volatile-lru |
| 단편화 | activedefrag yes (ratio > 1.5일 때) |
| Lazy-free | 모든 lazyfree 옵션 활성화 |
| 키 설계 | 짧은 키 이름, Hash 활용, 버켓팅 고려 |
| 모니터링 | used_memory, fragmentation_ratio, evicted_keys 추적 |
정리
Redis 메모리 최적화의 핵심은 listpack 인코딩을 최대한 유지하는 것입니다. Hash 버켓팅으로 키 수를 줄이고, 짧은 키 이름과 바이너리 직렬화로 바이트를 절약하세요. Eviction은 용도에 맞게 LFU/LRU를 선택하고, TTL에 지터를 추가해 thundering herd를 방지합니다. lazyfree를 활성화하고 큰 키는 UNLINK로 삭제하여 블로킹을 피하는 것이 프로덕션 운영의 기본입니다.