많은 프로젝트에서 캐시가 유용할 수 있지만 특히 클라이언트 측에서는 간과되는 경우가 많다는 것을 알게 되었습니다. 클라이언트 측 캐싱은 대기 시간을 줄이고 반복되는 서버 요청을 오프로드하여 사용자 경험을 향상시키는 데 중요합니다. 예를 들어 무한 스크롤 기능이 있거나 자주 업데이트되는 대시보드가 있는 애플리케이션에서는 이전에 가져온 데이터를 캐싱하면 불필요한 API 호출을 방지하여 보다 원활한 상호 작용과 빠른 렌더링 시간을 보장합니다.
최근 프로젝트 중 하나에서는 캐시를 구현하여 API 호출량이 40% 이상 감소하여 성능이 눈에 띄게 향상되고 비용이 절감되었습니다. 이는 클라이언트 측 캐싱이 기본 최적화 전략으로 간주되어야 하는 이유를 강조합니다. 캐시는 개발 시간 제약이나 기타 우선순위로 인해 상대적으로 간단한 구현으로 성능에 큰 영향을 주음에도 불구하고 마지막으로 고려되는 기능 중 하나가 되는 경향이 있습니다.
캐시는 정적 콘텐츠용 CDN인 Redis를 사용하는 백엔드 캐싱부터 클라이언트의 인메모리 캐시, 심지어 지속성을 위해 localStorage 또는 IndexedDB를 사용하는 등 아키텍처의 다양한 수준에서 구현될 수 있습니다. 이상적으로는 이러한 전략을 결합하여 데이터베이스와 API의 로드와 비용을 줄이고 특히 이전에 이미 가져온 데이터의 경우 클라이언트-서버 요청의 지연을 줄여야 합니다.
이 기사에서는 JavaScript에서 TTL(Time-to-Live) 지원을 통해 LRU(Least Recent Used) 캐시를 설계 및 구현하는 방법을 살펴보고 내 adev-lru와 유사한 패키지를 생성합니다. 마지막에는 효과적인 캐싱 솔루션의 핵심 원칙과 기능을 보여주는 실제 사례를 갖게 될 것입니다.
LRU(Least Recent Used) 캐시는 가장 최근에 액세스한 항목을 메모리에 유지하면서 용량이 초과되면 가장 최근에 액세스한 항목을 제거하도록 합니다. 이 전략은 사용 순서를 유지함으로써 작동합니다. 각 액세서리는 캐시에서 항목의 위치를 업데이트하고 가장 적게 액세스한 항목을 먼저 제거합니다.
다른 캐싱 전략과 비교할 때 LRU는 단순성과 효율성의 균형을 유지하므로 최근 사용량을 향후 액세스에 대한 신뢰할 수 있는 지표로 삼는 시나리오에 적합합니다. 예를 들어 API 응답, 썸네일 또는 자주 액세스하는 사용자 기본 설정을 캐시하는 애플리케이션은 LRU를 활용하여 제거 프로세스를 지나치게 복잡하게 하지 않고도 중복 가져오기 작업을 줄일 수 있습니다.
액세스 빈도를 추적하고 추가 기록이 필요한 LFU(최소 자주 사용)와 달리 LRU는 이러한 복잡성을 피하면서 많은 실제 사용 사례에서 여전히 뛰어난 성능을 달성합니다. 마찬가지로 FIFO(선입 선출) 및 MRU(최근 사용)는 대체 퇴거 정책을 제공하지만 최근 활동이 중요한 사용 패턴과 잘 맞지 않을 수 있습니다. 구현 시 LRU와 TTL(Time-to-Live) 지원을 결합함으로써 데이터에 자동 만료가 필요한 시나리오도 처리하고 라이브 대시보드나 스트리밍 서비스와 같은 동적 환경에서의 적용 가능성을 더욱 향상시킵니다. 최신 데이터에 대한 액세스가 중요한 애플리케이션에 특히 유용합니다.
LRUCache 클래스는 효율적이고 유연한 구성을 지원하며 자동 제거를 처리하도록 구축되었습니다. 다음은 몇 가지 주요 방법입니다.
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
이 방법을 사용하면 애플리케이션에 캐시 인스턴스가 하나만 있도록 하여 리소스 관리를 단순화하는 설계 선택이 됩니다. 캐시를 싱글톤으로 구현함으로써 중복된 메모리 사용을 방지하고 애플리케이션 전체에서 일관된 데이터를 보장합니다. 이는 충돌을 방지하고 추가 조정 논리 없이 동기화를 보장하므로 여러 구성 요소 또는 모듈이 동일한 캐시된 데이터에 액세스해야 하는 시나리오에서 특히 유용합니다. 용량을 지정하지 않으면 기본값은 10입니다.
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
이 방법은 캐시에 항목을 추가하거나 업데이트합니다. 키가 이미 존재하는 경우 해당 항목이 제거되고 캐시 앞에 다시 추가됩니다. 이를 위해 캐시는 이중 연결 목록을 사용하여 데이터를 노드로 저장하고 목록 끝(Tail)에서 데이터를 삭제하고 목록의 시작 부분(Head)으로 이동하여 상수 O를 보장하는 기능을 유지합니다. (1) 모든 노드의 데이터를 읽으려면 해시 테이블을 사용하여 목록의 각 노드에 대한 포인터를 저장합니다. 이 프로세스는 최근에 액세스한 항목에 항상 우선순위를 부여하여 효과적으로 "가장 최근에 사용된 항목"으로 표시함으로써 LRU 원칙과 일치합니다. 이를 통해 캐시는 정확한 사용 순서를 유지하며 이는 용량이 초과될 때 제거 결정을 내리는 데 중요합니다. 이 동작을 통해 리소스가 최적으로 관리되어 자주 액세스하는 데이터에 대한 검색 시간이 최소화됩니다. 키가 이미 존재하는 경우 해당 항목이 맨 앞으로 이동되어 최근에 사용한 것으로 표시됩니다.
public get(key: string): T | undefined { const node = this.hash.get(key); const now = Date.now(); if (node == null || node.ttl < now) { return undefined; } this.evict(node); this.prepend(node.key, node.value, node.ttl); return node.value; }
이 방법은 저장된 항목을 검색합니다. 항목이 만료된 경우 캐시에서 제거됩니다.
캐시 효율성을 평가하기 위해 적중률, 실패, 제거와 같은 성능 지표를 구현했습니다.
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
이 방법은 모든 항목을 지우고 캐시 상태를 재설정합니다.
제 구현에서는 T | 정의되지 않은 경우 더 기능적인 접근 방식을 선호하는 사람들을 위해 모나드 옵션의 인스턴스를 반환합니다. 또한 로깅 목적으로 캐시의 모든 작업을 추적하기 위해 Writer 모나드를 추가했습니다.
이 저장소에서 매우 잘 설명된 이 알고리즘과 관련된 다른 모든 방법을 볼 수 있습니다: https://github.com/Armando284/adev-lru
LRU 캐시가 유일한 옵션은 아닙니다. 올바른 캐싱 알고리즘을 선택하는 것은 애플리케이션의 특정 요구 사항과 액세스 패턴에 따라 크게 달라집니다. 다음은 일반적으로 사용되는 다른 캐싱 전략과 LRU를 비교한 내용 및 각각의 사용 시기에 대한 지침입니다.
LRU는 단순성과 효율성 사이의 균형을 유지하므로 최근 활동이 향후 사용과 밀접한 관련이 있는 애플리케이션에 이상적입니다. 예를 들면 다음과 같습니다.
반대로, 액세스 패턴에서 빈도나 삽입 순서가 더 관련성이 높은 것으로 나타나면 LFU 또는 FIFO와 같은 알고리즘이 더 나은 선택일 수 있습니다. 이러한 장단점을 평가하면 캐싱 전략이 애플리케이션의 목표 및 리소스 제약 조건에 부합하는지 확인할 수 있습니다.
인메모리 캐시를 구현하면 애플리케이션 성능이 크게 향상되어 응답 시간이 줄어들고 사용자 경험이 향상될 수 있습니다.
전체 LRU 캐시가 작동하는 모습을 보려면 내 npm 패키지(https://www.npmjs.com/package/adev-lru)를 사용하세요. 또한 계속해서 개선할 수 있도록 피드백을 받고 싶습니다.
패키지를 사용해보고 더 많은 도움을 주고 싶다면 생각을 공유하거나 기부해 주세요?!
위 내용은 메모리 내 캐시를 만드는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!