출시·고도화 중
Algorithm
LRU 캐시는 가득 찼을 때 가장 오랫동안 쓰이지 않은 항목을 버리는 캐시로, 해시 맵과 이중 연결 리스트로 조회와 저장을 O(1)에 처리합니다.
LRU(Least Recently Used) 캐시는 용량이 정해진 캐시가 가득 찼을 때 가장 오래전에 사용한 항목부터 제거하는 교체 정책과 그 자료 구조를 말합니다. 최근에 쓴 데이터는 곧 다시 쓰일 가능성이 높다는 시간 지역성에 기대어 있으며, FIFO와 달리 조회할 때마다 항목의 순서를 새로 고칩니다. 표준 구현은 키로 노드를 찾는 해시 맵과 사용 순서를 유지하는 이중 연결 리스트를 함께 써서 get과 put을 모두 평균 O(1)에 처리합니다.
LRU는 CPU 캐시와 운영체제의 페이지 캐시, 데이터베이스 버퍼 풀, Redis의 메모리 제거 정책, CDN과 브라우저 캐시 등 시스템의 거의 모든 층에서 그대로 또는 근사 형태로 쓰입니다. Python의 functools.lru_cache와 OrderedDict, Java의 LinkedHashMap처럼 언어 표준 라이브러리에도 들어 있습니다. 또한 해시 맵과 연결 리스트를 함께 다루는 설계 문제라서 코딩 테스트와 기술 면접에 자주 나옵니다.
먼저 FIFO, LRU, LFU, TTL 같은 교체 정책의 차이와 적중률 개념을 익힌 뒤, 작은 요청 순서를 손으로 추적해 보는 것이 좋습니다. 이어서 보초 노드를 둔 이중 연결 리스트와 딕셔너리로 직접 구현하고, OrderedDict나 LinkedHashMap 같은 라이브러리 버전과 비교해 봅니다. 마지막으로 스레드 안전성, 캐시 무효화, TTL과의 조합처럼 실무에서 부딪히는 문제를 연습 문제로 다뤄 보면 됩니다.
조회나 저장에 성공할 때마다 항목을 가장 최근 쪽으로 옮기고, 공간이 모자라면 반대쪽 끝의 항목을 제거합니다.
맵은 키로 노드를 바로 찾고, 리스트는 노드를 O(1)에 떼어 내고 앞에 붙여 순서를 유지합니다.
get과 put 모두 정해진 개수의 포인터만 바꾸므로 평균 상수 시간에 끝나며, 공간은 용량에 비례합니다.
실무에서는 CLOCK, 의사 LRU, 표본 추출 LRU, 2Q, W-TinyLFU처럼 비용을 줄이거나 스캔에 강한 변형을 많이 씁니다.
딕셔너리가 키를 노드에 연결하고, 보초 노드 head와 tail 사이의 이중 연결 리스트가 사용 순서를 유지합니다. get은 노드를 맨 앞으로 옮기고, put은 가득 찼을 때 tail 바로 앞의 노드를 제거합니다. python lru_cache.py로 실행하면 "b"가 제거되어 None 1 3이 출력됩니다.
lru_cache.py
class Node:
__slots__ = ("key", "value", "prev", "next")
def __init__(self, key=None, value=None):
self.key, self.value = key, value
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.map = {} # key -> Node
self.head, self.tail = Node(), Node() # sentinels
self.head.next, self.tail.prev = self.tail, self.head
def _unlink(self, node):
node.prev.next, node.next.prev = node.next, node.prev
def _push_front(self, node):
node.prev, node.next = self.head, self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key, default=None):
node = self.map.get(key)
if node is None:
return default
self._unlink(node) # touch: move to the front
self._push_front(node)
return node.value
def put(self, key, value):
if self.capacity <= 0:
return
node = self.map.get(key)
if node is not None:
node.value = value
self._unlink(node)
self._push_front(node)
return
if len(self.map) >= self.capacity: # evict the least recently used
lru = self.tail.prev
self._unlink(lru)
del self.map[lru.key]
node = Node(key, value)
self.map[key] = node
self._push_front(node)
cache = LRUCache(2)
cache.put("a", 1)
cache.put("b", 2)
cache.get("a") # "a" is now the most recent
cache.put("c", 3) # full: evicts "b"
print(cache.get("b"), cache.get("a"), cache.get("c")) # None 1 3
python lru_cache.py설치부터 LRU 캐시 의 핵심 개념까지, 여섯 장으로 차근차근 따라 합니다.
LRU 캐시 에 관해 묻고, 경험을 나누고, 의견을 주고받는 곳입니다.
아직 토론이 없습니다. 첫 이야기를 시작해 보세요.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.