Java 아키텍처·Spring 용어 사전
캐시maxmemory-policy · 축출

축출 정책

메모리가 가득 찼을 때 무엇을 버리나. LRU·LFU 선택이 적중률을 가른다.

maxmemory 에 닿으면 새 데이터를 넣을 자리를 만들어야 한다. 무엇을 버릴지 고르는 규칙이 축출 정책이다.

정책버리는 대상
noeviction아무것도 안 버리고 쓰기에 에러를 낸다
allkeys-lru전체 키 중 오래 안 쓴
allkeys-lfu전체 키 중 적게 쓴
volatile-lru / volatile-lfuTTL 이 걸린 키 중에서만 위와 같이
allkeys-random무작위
volatile-ttl만료가 임박한 것부터

만료와 축출은 다르다

자주 섞이는데 별개다.

  • 만료(expiration) — 내가 건 TTL 이 지나서 지워지는 것. 메모리와 무관하다
  • 축출(eviction) — 메모리가 가득 차서 TTL 이 남아 있는 키까지 밀어내는 것

TTL 을 넉넉히 걸어 뒀는데 데이터가 일찍 사라진다면 대개 축출이다. 로그의 evicted_keys 지표를 봐야 구분된다.

LRU 와 LFU 는 무엇이 다른가

  • LRU(Least Recently Used)마지막으로 언제 썼나. 최근에 쓰였으면 남긴다
  • LFU(Least Frequently Used)얼마나 자주 쓰였나. 많이 쓰였으면 남긴다

배치 작업이 한밤중에 수만 건을 한 번씩 훑고 지나갔다고 하자. LRU 에게 그 키들은 전부 "방금 쓴 것" 이라 살아남고, 정작 종일 조회되던 인기 상품이 밀려난다. 아침에 적중률이 뚝 떨어져 있다. 이것이 캐시 오염이다.

LFU 는 접근 횟수를 보므로 한 번 훑고 지나간 키가 인기 항목을 밀어내지 못한다. 반대로 트렌드가 빠르게 바뀌는 데이터라면 LFU 가 과거의 인기에 매달려 새 항목을 못 들이기도 한다. Redis 의 LFU 는 카운터를 시간에 따라 감쇠시켜(lfu-decay-time) 이 문제를 줄인다.

정확한 LRU 가 아니다

정확한 LRU 는 모든 키를 접근 순서로 이어 둔 연결 리스트를 유지해야 한다. 키를 건드릴 때마다 그 노드를 맨 앞으로 옮겨야 하니, 포인터 메모리도 들고 갱신 비용도 든다. 키가 수천만 개면 그 자체가 부담이다.

Redis 는 대신 키를 몇 개 무작위로 뽑아 그중 가장 오래된 것을 버린다(maxmemory-samples, 기본 5). 표본을 늘리면 정확해지지만 CPU 를 더 쓴다. 정확도를 조금 내주고 비용을 크게 줄인 맞바꿈이고, 캐시라는 용도에서는 완벽한 LRU 가 필요하지 않다는 판단이 깔려 있다.

어느 것을 고르나

  • 순수 캐시allkeys-lruallkeys-lfu. 지워져도 DB 에서 다시 채운다
  • 세션·분산 락처럼 사라지면 안 되는 것이 섞여 있다volatile-* 로 두고 캐시성 키에만 TTL 을 건다. TTL 없는 키는 축출 대상에서 빠진다
  • noeviction 은 저장소로 쓸 때다. 가득 차면 쓰기가 실패하므로 메모리 감시가 필수다

주의할 것은 volatile-* 인데 TTL 걸린 키가 하나도 없으면 버릴 대상이 없어 noeviction 처럼 에러가 난다는 점이다.

면접 함정

  • "TTL 이 지나야 지워진다" → 메모리가 차면 TTL 이 남아 있어도 축출된다.
  • "LFU 가 항상 낫다" → 접근 패턴이 빨리 변하면 LRU 가 유리하다.
  • "Redis 의 LRU 는 정확하다" → 표본 기반 근사다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 캐시·Redis — 어사이드·쓰기 전략