[250302] LRU Cache

Table of Contents

1 LRU Cache란?

LRU(Least Recently Used) Cache는 가장 최근에 사용되지 않은 데이터를 제거하는 캐시 eviction 정책을 사용한다.

O(1) 시간 복잡도를 달성하기 위해 이중연결리스트와 해시맵을 사용한다.

2 소스코드

class Node:
    def __init__(self, key, value):
	self.key = key
	self.value = value
	self.prev = None  # 이전 노드
	self.next = None  # 다음 노드

class LRUCache:
    def __init__(self, capacity):
	self.capacity = capacity  # 캐시 용량
	self.cache = {}  # key -> Node 매핑
	# 더미 헤드와 테일로 이중 연결 리스트 관리
	self.head = Node(0, 0)  # 더미 헤드
	self.tail = Node(0, 0)  # 더미 테일
	self.head.next = self.tail
	self.tail.prev = self.head

    def _remove(self, node):
	"""이중 연결 리스트에서 노드 제거"""
	node.prev.next = node.next
	node.next.prev = node.prev

    def _add(self, node):
	"""노드를 헤드 바로 다음에 추가 (가장 최근 사용)"""
	node.prev = self.head
	node.next = self.head.next
	self.head.next.prev = node
	self.head.next = node

    def get(self, key):
	"""키에 해당하는 값 반환"""
	if key in self.cache:
	    node = self.cache[key]
	    self._remove(node)  # 기존 위치에서 제거
	    self._add(node)     # 가장 최근 사용 위치로 이동
	    return node.value
	return -1

    def put(self, key, value):
	"""키-값 쌍 추가 또는 업데이트"""
	if self.capacity == 0:
	    return

	if key in self.cache:
	    self._remove(self.cache[key])  # 기존 노드 제거
	elif len(self.cache) >= self.capacity:
	    # 캐시가 가득 찼을 때 가장 오래된 항목 제거
	    lru_node = self.tail.prev
	    self._remove(lru_node)
	    del self.cache[lru_node.key]

	# 새 노드 추가
	new_node = Node(key, value)
	self._add(new_node)
	self.cache[key] = new_node

# 사용 예시
lru = LRUCache(2)
lru.put(1, 1)    # 캐시: {1=1}
lru.put(2, 2)    # 캐시: {1=1, 2=2}
print(lru.get(1))  # 출력: 1 (1이 최근 사용됨)
lru.put(3, 3)    # 2 제거, 캐시: {1=1, 3=3}
print(lru.get(2))  # 출력: -1
print(lru.get(1))  # 출력: 1
lru.put(4, 4)    # 3 제거, 캐시: {1=1, 4=4}
print(lru.get(3))  # 출력: -1
print(lru.get(4))  # 출력: 4

3 코드 설명

클래스 구조

Node:

  • 캐시 항목을 나타냅니다.
  • 속성: key, value, prev, next (이중 연결 리스트 포인터).

LRUCache:

  • LRU 캐시의 전체 로직을 관리합니다.
  • 주요 속성:
    • capacity: 최대 용량.
    • cache: 키로 노드를 빠르게 찾기 위한 해시 맵.
    • head, tail: 이중 연결 리스트의 더미 노드.

주요 메서드

  • _remove(self, node): 노드를 이중 연결 리스트에서 제거.
  • _add(self, node): 노드를 리스트의 가장 앞(최근 사용 위치)에 추가.
  • get(self, key): 키에 해당하는 값을 반환하고, 해당 노드를 최근 사용 위치로 이동.
  • put(self, key, value): 키-값 쌍을 추가하거나 업데이트하며, 캐시가 가득 차면 가장 오래된 항목을 제거.

시간 복잡도

  • get: O(1) - 해시 맵으로 노드 찾기 및 리스트 이동 모두 상수 시간.
  • put: O(1) - 삽입, 제거, 업데이트 모두 상수 시간.

4 LRU가 적합한 경우

LRU는 "최근에 사용된 데이터가 앞으로도 사용될 가능성이 높다"는 시간적 지역성(Temporal Locality) 원칙을 기반으로 동작합니다. 따라서 다음과 같은 상황에서 적합.

  1. 시간적 지역성이 강한 작업
    • 사용 사례: 웹 브라우저의 캐시, 데이터베이스 쿼리 캐시, 운영체제 페이지 캐시.
    • 이유: 사용자가 최근에 방문한 웹 페이지나 최근에 실행한 쿼리는 다시 사용될 가능성이 높음. LRU는 이러한 최근 데이터를 유지하고 오래된 데이터를 제거하여 효율성을 높임.
  2. 짧은 시간 내 반복 접근 패턴
    • 사용 사례: 스트리밍 서비스의 버퍼링, 실시간 로그 분석.
    • 이유: 특정 데이터(예: 최근 재생된 비디오 프레임)가 짧은 시간 동안 반복적으로 접근될 때, LRU는 이를 캐시에 유지하여 빠른 접근을 보장.
  3. 고정된 데이터셋 크기에서의 효율성
    • 사용 사례: 메모리 제한이 있는 임베디드 시스템, 소규모 캐시.
    • 이유: LRU는 캐시 크기가 작아도 최근 사용된 데이터를 우선적으로 유지하므로, 제한된 리소스 환경에서 효율적.
  4. 단순하고 빠른 구현이 필요한 경우
    • 사용 사례: 프로토타이핑, 성능 최적화가 중요한 애플리케이션.
    • 이유: LRU는 LFU보다 구현이 간단하고 모든 연산이 O(1)에 수행되므로, 빠른 개발과 실행 속도가 필요한 환경에 적합.

5 LRU가 부적합한 경우

  1. 라운드 로빈 패턴: 데이터가 순차적으로 한 번씩만 사용된다면, 모든 항목이 계속 교체되어 캐시 적중률이 떨어짐(LFU가 더 적합).
  2. 빈도 기반 우선순위: 자주 사용되지만 최근에 사용되지 않은 데이터가 제거될 수 있음(LFU가 더 유리).

6 한계 극복 방안

  • LFU(Least Frequently Used): 자주 사용되지 않는 데이터를 제거하여 라운드 로빈 패턴이나 빈도 기반 데이터에 더 적합.
  • 2Q 또는 ARC(Adaptive Replacement Cache): LRU와 LFU의 장점을 결합하여 시간적/빈도적 지역성을 모두 고려.
  • SLRU(Segmented LRU): 캐시를 최근 사용/오래된 사용으로 세분화하여 성능을 개선.
  • 캐싱 라이브러리 활용: Redis, Memcached와 같은 라이브러리는 LRU를 기반으로 최적화된 알고리즘을 제공.

7 결론

LRU는 최근 사용 패턴이 중요한 환경에서 강력하며, 구현이 간단하고 모든 연산이 O(1)로 빠름. 반면, LFU는 사용 빈도를 기준으로 데이터를 유지하므로, 자주 사용되는 데이터를 장기적으로 보존해야 하는 경우에 더 적합.

Date: 2025-03-02 Sun 00:00

Author: 남영환

Created: 2025-03-27 Thu 15:43

Emacs 27.2 (Org mode 9.4.4)

Validate