백엔드 데이터 인프라 55편. Redis 키 만료 — EXPIRE·PEXPIRE·TTL·PERSIST 명령어와, 메모리 가득 찼을 때 어떤 키부터 쫓아낼지 결정하는 maxmemory-policy 8가지(noeviction·allkeys-lru·allkeys-lfu·volatile-lru 등) 정확히 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 130편 중 55편이에요. 54편 으로 자주 쓰는 6종 데이터 타입(49~54편) 을 끝냈다면, 이번 55편부터는 운영 영역 — 키 만료(TTL, Time To Live — 키 자동 삭제 시한) 와 메모리 가득 찼을 때 어떻게 동작하나(eviction policy — 메모리 부족 시 키 추방 정책) 를 풀어 가요. 캐시 운영의 핵심.
TTL·Eviction이 어렵게 느껴지는 이유
캐시 운영에서 가장 자주 깨지는 자리. 두 가지가 처음 잡히지 않아요.
첫째, TTL 명령어가 8개나 됩니다. EXPIRE·PEXPIRE·EXPIREAT·PEXPIREAT·TTL·PTTL·EXPIRETIME·PEXPIRETIME·PERSIST. 초 vs 밀리초 · 상대 시간 vs 절대 시간 · 조회 형식 차이 — 차이가 미세해서 어느 걸 써야 할지 헷갈려요.
둘째, eviction policy 가 8가지. noeviction · allkeys-lru · allkeys-lfu · allkeys-random · volatile-lru · volatile-lfu · volatile-random · volatile-ttl. 이름이 비슷비슷한데 "실수로 잘못 박으면 운영 사고" 가 자주 일어나는 영역이에요. 특히 "왜 갑자기 캐시가 다 날아갔지?" 의 절반은 eviction 정책 오설정.
이 글에서 TTL 명령어 8개를 3개 그룹 으로 묶어 정리하고, eviction policy 8가지를 선택 결정 트리 로 정리해요. 한 번 잡으면 운영 사고가 안 나는 영역.
TTL 명령어 — 3그룹으로 묶어 외우기
그룹 1: 만료 설정 (EXPIRE 계열, 4개)
> SET session:abc "user42"
> EXPIRE session:abc 3600 # 3600초 (1시간) 후 만료
(integer) 1
> PEXPIRE session:abc 3600000 # 3,600,000 밀리초 = 1시간
(integer) 1
> EXPIREAT session:abc 1747500000 # 절대 시각 (Unix timestamp, 초)
(integer) 1
> PEXPIREAT session:abc 1747500000000 # 절대 시각 밀리초
(integer) 1
| 명령어 | 단위 | 기준 |
|---|---|---|
EXPIRE |
초 | 상대 (지금부터 N초 후) |
PEXPIRE |
밀리초 | 상대 |
EXPIREAT |
초 | 절대 (Unix timestamp) |
PEXPIREAT |
밀리초 | 절대 |
옵션 (Redis 7+): NX·XX·GT·LT
> EXPIRE session:abc 7200 GT # 기존 TTL 보다 클 때만 갱신
그룹 2: TTL 조회 (4개)
> TTL session:abc # 남은 시간 (초)
(integer) 3598
> PTTL session:abc # 남은 시간 (밀리초)
(integer) 3598123
> EXPIRETIME session:abc # 절대 만료 시각 (초)
(integer) 1747500000
> PEXPIRETIME session:abc # 절대 만료 시각 (밀리초)
(integer) 1747500000000
여기서 시험 함정이 하나 있어요 — TTL 반환값의 3가지 경우:
- 양수 — 남은 초
-1— 키는 있는데 TTL 없음 (영구)-2— 키 없음
-1 과 -2 차이를 모르고 모두 없음으로 처리 하면 영구 키 가 불필요하게 만료 되는 버그가 자주 나요.
그룹 3: 만료 제거
> PERSIST session:abc # TTL 제거 (영구 키로)
(integer) 1
> TTL session:abc
(integer) -1 # 이제 영구
SET ... KEEPTTL 옵션도 값 갱신 시 만료 유지. (49편 String 참고)
SET 과 함께 한 줄로 — SET ... EX N
대부분의 실무에서 SET + EXPIRE 를 따로 부르지 않고 한 줄로 끝내요:
> SET session:abc "user42" EX 3600
OK
> SET rate:user42 1 PX 1000 # 1초 (밀리초)
> SET cache:foo "value" EXAT 1747500000 # 절대 시각
장점은 세 가지로 압축돼요. 먼저 atomic (원자적 — 중간에 끊기지 않음) 하니까 SET 과 EXPIRE 사이에 서버가 다운돼도 만료 없는 키 가 남는 버그가 차단되고, 다음으로 명령이 한 번이라 네트워크 왕복도 한 번이며, 마지막으로 코드도 깔끔해져요.
SET 옵션 = EX (초) · PX (밀리초) · EXAT · PXAT · KEEPTTL. 거의 모든 만료 있는 SET 은 이 형태로 쓰는 게 표준.
한 줄 정리 — TTL은 SET ... EX N 으로 한 번에 박는 게 표준. 따로 EXPIRE 부르는 건 이미 있는 키 의 TTL 만 갱신할 때.
Lazy + Active — Redis 만료 처리 메커니즘
여기서 가끔 묻는 질문 — "EXPIRE 박은 키, 정확히 그 시점에 메모리에서 사라지나요?". 답은 "정확히 그 시점은 아니고, 두 가지 방식으로 처리".
Lazy Expiration
키에 접근할 때 만료됐는지 체크 → 만료됐으면 즉시 삭제 + nil 반환.
> SET temp "x" EX 1
> # 2초 후
> GET temp
(nil) # 이 시점에 만료 + 삭제됨
접근 시에만 처리하니 비용은 0인데, 대신 접근 안 되는 만료 키 가 메모리에 계속 남는 게 약점이에요.
Active Expiration
Redis 가 백그라운드 스레드에서 주기적으로 임의의 키 20개 샘플링 → 만료된 것 제거. 만료 비율이 25% 넘으면 다시 샘플. 초당 100ms 정도 CPU 사용.
장점은 접근 안 되는 만료 키도 결국 제거 된다는 점이고, 단점은 정확히 만료 시점에 제거되지는 않는다는 점.
결과 — 만료된 키가 일시적으로 메모리에 남아 있을 수 있음. 그래도 접근하면 보이지 않고(nil), 결국 active 가 청소 함. 정확한 바이트 단위 메모리 관리 가 필요한 환경에서는 이 메커니즘을 이해해야.
maxmemory — 메모리 한계 설정
maxmemory 4gb # 최대 4GB까지 사용
redis.conf 또는 CONFIG SET. 도달하면 eviction policy 가 발동.
Eviction Policy 8가지 — 정확한 선택
가장 중요한 자리. 이름 패턴 두 축:
- allkeys- vs volatile- — 모든 키 대상 vs TTL 박힌 키만 대상
- lru vs lfu vs random vs ttl — 어떤 기준으로 쫓아낼지
8가지 풀 리스트
| Policy | 의미 |
|---|---|
| noeviction | 쫓아내지 않음 — 메모리 가득 차면 쓰기 명령 에러 (DB 모드) |
| allkeys-lru | 모든 키 중 최근에 적게 쓴 것(LRU) 부터 제거 |
| allkeys-lfu | 모든 키 중 자주 안 쓰는 것(LFU) 부터 제거 |
| allkeys-random | 모든 키 중 무작위 제거 |
| volatile-lru | TTL 있는 키 중 LRU |
| volatile-lfu | TTL 있는 키 중 LFU |
| volatile-random | TTL 있는 키 중 무작위 |
| volatile-ttl | TTL 있는 키 중 만료 시각이 가장 가까운 것부터 |
LRU vs LFU 차이
- LRU (Least Recently Used — 가장 오래 안 본 것) — 마지막 접근 시각 기준. 최근에 접근 안 한 것 부터 제거. 시간적 지역성 가정.
- LFU (Least Frequently Used — 가장 적게 본 것) — 접근 빈도 기준. 덜 자주 접근한 것 부터 제거. 접근 빈도가 의미 있는 환경 (예: 주말마다 보는 콘텐츠).
LFU 가 이론적으로 더 정교 하지만, LRU 도 충분히 좋고 오버헤드 적음. 처음 도입은 LRU 가 무난.
한 줄 암기 — LRU = 언제 봤나, LFU = 얼마나 자주 봤나.
Volatile vs Allkeys
- volatile- — TTL 안 박은 키는 절대 안 사라짐. 영구 보관 키와 캐시* 키를 같은 인스턴스에 섞을 때 안전.
- allkeys- — TTL 무관, 모든 키가 eviction 대상. 순수 캐시* 인스턴스에 적합.
여기서 정말 중요한 시험 함정 — volatile-* 정책인데 TTL 없는 키만 가득 차면 → eviction 못 함 → noeviction 처럼 동작. "왜 갑자기 쓰기 실패?" 의 자주 보이는 원인.
선택 결정 트리
이 Redis 인스턴스의 용도는?
├─ 순수 캐시 (날아가도 OK)
│ ├─ 접근 빈도 의미 있음 → allkeys-lfu
│ └─ 시간적 지역성 → allkeys-lru (무난한 기본)
├─ 캐시 + 영구 데이터 혼합
│ ├─ TTL 있는 캐시만 쫓아냄 → volatile-lru / volatile-lfu
│ └─ 만료 임박 우선 쫓음 → volatile-ttl
└─ DB 모드 (날아가면 안 됨)
└─ noeviction (쓰기 실패가 데이터 손실보다 나음)
자주 보이는 조합은 셋. 캐시 전용 인스턴스 면 allkeys-lru 나 allkeys-lfu 로 가고, 세션 저장소 + 캐시 처럼 섞여 있으면 세션에 TTL 이 박혀 있으니 volatile-lru 가 안전해요. Redis 를 DB 처럼 쓰는 자리면 noeviction 으로 두고 메모리 부족 시 쓰기 에러를 알람 트리거로 받는 게 정석.
기본값 = noeviction. 명시적으로 안 바꾸면 메모리 가득 시 쓰기 에러 가 발생해 조용히 캐시가 날아가는 사고가 안 난다는 안전 디폴트.
설정 방법
# 런타임 변경 (재시작 X)
> CONFIG SET maxmemory 4gb
OK
> CONFIG SET maxmemory-policy allkeys-lru
OK
# 현재 설정 조회
> CONFIG GET maxmemory
1) "maxmemory"
2) "4294967296"
> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"
영구 적용은 redis.conf 수정 + 재시작 또는 CONFIG REWRITE:
# redis.conf
maxmemory 4gb
maxmemory-policy allkeys-lru
메모리 모니터링 — INFO memory
> INFO memory
# Memory
used_memory:1048576 # 사용 중 (바이트)
used_memory_human:1.00M
used_memory_rss:1572864 # OS 가 본 실제 사용 (fragmentation 포함)
used_memory_peak:2097152
maxmemory:4294967296 # 한계
maxmemory_human:4.00G
mem_fragmentation_ratio:1.5 # 1.5 = 50% fragmentation
운영 모니터링에서 매번 보는 메트릭. used_memory_rss / used_memory 가 1.5 넘어가면 fragmentation (메모리 단편화 — 빈틈이 많아 실사용보다 큰 상태) 큼 → Redis 4.0+ active defragmentation 활성화 검토.
시험 직전 한 번 더 — TTL · Eviction 함정 압축 노트
- TTL 명령 4가지 setter =
EXPIRE(초)·PEXPIRE(ms)·EXPIREAT(절대 초)·PEXPIREAT(절대 ms) - TTL 조회 4가지 =
TTL(초)·PTTL(ms)·EXPIRETIME·PEXPIRETIME TTL반환 = 양수(남은 초) / -1(영구) / -2(키 없음)-1vs-2혼동이 영구 키 실수 만료 버그의 원인PERSIST= TTL 제거 → 영구 키로SET ... EX N= SET + EXPIRE 한 줄, atomic — 실무 표준SET ... KEEPTTL= 값 갱신 시 만료 유지- 만료 처리 = Lazy (접근 시) + Active (백그라운드 샘플링)
- 만료 키 = 일시적으로 메모리 남을 수 있음, 결국 정리
maxmemory= Redis 메모리 한계 설정- 도달 시
maxmemory-policy발동 - 8가지 policy =
noeviction/allkeys-{lru,lfu,random}/volatile-{lru,lfu,random,ttl} - LRU = 마지막 접근 시각 / LFU = 접근 빈도
- LFU 가 정교, LRU 가 무난한 기본
- volatile- = TTL 있는 키만 대상 — TTL 없는 키는 안 사라짐*
- allkeys- = 모든 키 대상 — 순수 캐시* 에 적합
- 함정 = volatile-* + TTL 없는 키만 가득 = eviction 못 함 = 쓰기 실패
- 기본값 = noeviction (메모리 부족 시 쓰기 에러 → 알람 트리거)
- 선택 결정 — 캐시 전용=allkeys-lru/lfu / 캐시+영구=volatile-lru / DB모드=noeviction
- 런타임 변경 =
CONFIG SET maxmemory-policy ... - 영구 적용 =
redis.conf수정 또는CONFIG REWRITE - 모니터링 =
INFO memory(used_memory·peak·fragmentation_ratio) - fragmentation_ratio > 1.5 = active defragmentation 검토
공식 문서: Redis Eviction 에서 maxmemory-policy 8가지 사양을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 50편 — Redis Hash + 객체 캐싱 패턴
- 51편 — Redis List + 큐·캡 리스트 패턴
- 52편 — Redis Set + 집합 연산 패턴
- 53편 — Redis Sorted Set + 랭킹·Sliding Window Rate Limiter 패턴
- 54편 — Redis Stream + Consumer Group + Kafka 비교
다음 글: