layercache가 무엇인가요?

Node.js에서 Memory → Redis → Disk를 하나의 API로 묶어주는 멀티레이어 캐시 라이브러리입니다.

캐시 히트 시에는 가장 빠른 레이어에서 값을 꺼내고 상위 레이어를 자동으로 채웁니다. 미스 시에는 동시 요청이 100개 들어와도 fetcher는 딱 한 번만 실행됩니다.

왜 만들었나요?

Node.js 서비스를 운영하다 보면 캐싱 레이어를 쌓는 방식은 대체로 비슷한 수순을 밟습니다. 인메모리 캐시로 시작하고, 인스턴스가 늘어나면 Redis를 붙이고, 그러다 stampede 문제를 만나고, 인스턴스 간 캐시 불일치가 생기고... 각각은 해결할 수 있는 문제인데, 이걸 프로덕션 수준으로 한꺼번에 엮는 게 생각보다 손이 많이 갔습니다.

그래서 그 작업을 한 번만 제대로 해두자는 생각으로 만들었습니다.

주요 기능은 어떻게 되나요?

핵심 동작

  • 레이어드 읽기 + 자동 backfill (L1 미스 → L2 조회 → L1 채움)
  • Stampede prevention: 동시 요청 100개 → fetcher 실행 1회
  • Distributed single-flight: Redis 분산 락으로 인스턴스 간 중복 실행 제거
  • Redis pub/sub 기반 L1 invalidation bus (인스턴스 간 메모리 캐시 동기화)

무효화 / 신선도

  • 태그 기반 무효화, 와일드카드/접두어 무효화
  • Stale-while-revalidate, Stale-if-error
  • Sliding TTL, Adaptive TTL, Refresh-ahead

복원력

  • Graceful degradation: Redis 장애 시 레이어 스킵 후 자동 복구
  • Circuit breaker
  • Strict / best-effort 쓰기 정책

관측성

  • Prometheus exporter, OpenTelemetry 훅
  • 레이어별 지연 시간 측정, 이벤트 훅
  • Admin CLI (npx layercache stats|keys|invalidate)

프레임워크 통합
Express, Fastify, Hono, tRPC, GraphQL, Next.js

벤치마크 숫자가 궁금합니다

단일 코어 VM + 실제 Docker Redis 기준입니다.

| 시나리오 | 평균 지연 |
| L1 메모리 웜 히트 | 0.005 ms |
| L2 Redis 웜 히트 (1 KiB) | 0.193 ms |
| 캐시 없음 (DB 시뮬) | 5.030 ms |

  • HTTP 처리량: /layered 16,211 req/s vs /nocache 158 req/s
  • Stampede: 동시 75 요청 → origin fetch 5회 (캐시 없이는 375회)
  • Distributed single-flight: 동시 60 요청 → origin fetch 1회

전체 벤치마크 방법론과 raw 결과는 docs/benchmarking.md에 정리해두었습니다.

기존 라이브러리와는 뭐가 다른가요?

node-cache-manager, keyv, cacheable 모두 좋은 선택지입니다. 차이점을 간단히 정리하면:

  • Stampede prevention / Distributed single-flight: 세 라이브러리 모두 기본 제공하지 않습니다. layercache는 이 두 가지를 핵심으로 설계했습니다.
  • Cross-instance L1 invalidation: Redis pub/sub으로 인스턴스 간 메모리 캐시를 자동으로 동기화합니다. 멀티 인스턴스 환경에서 메모리 캐시를 안심하고 쓸 수 있습니다.
  • Auto backfill: 하위 레이어 히트 시 상위 레이어를 자동으로 채웁니다.
  • Graceful degradation + Circuit breaker: Redis가 죽어도 서비스가 살아있습니다.

설치 및 링크

npm install layercache  

설계 결정, 특히 single-flight 조율 방식이나 graceful degradation 동작에 대해 궁금하신 점 있으시면 편하게 질문해주세요.

댓글과 토론

좋은 라이브러리네요!
Redis가 설계에 포함된 이유가 있나요? 읽기용 인스턴스 여러개가 동시에 뜨는걸 가정하는 상황인가요? 그렇다면 (로컬)Disk가 Redis보다 앞 레이어에 배치되어야 하지 않을까요?

Redis가 들어간 건 서버가 여러 대일 때를 가정해서예요. 각 서버의 메모리는 서로 다른 값을 갖고 있을 수 있으니까, Redis가 "공유 진실" 역할을 합니다.

Disk가 Redis보다 뒤에 오는 건 Redis가 같은 로컬 네트워크에 있다는 가정 하에 더 빠르기 때문이에요. 벤치마크 기준으로 Disk는 ~2ms, Redis는 ~0.02ms거든요. 하지만 Redis가 멀리 있거나 네트워크가 나쁘면 로컬 Disk가 더 빠를 수 있고, 그럴 땐 순서를 바꾸는 게 맞아요. 라이브러리도 순서를 강제하지 않고 사용자가 직접 정하는 구조입니다.

Disk는 어디에 있든 간에 속도 경쟁보다는, Memory와 Redis가 다 죽었을 때 살아남는 마지막 보험 역할이 주목적이에요.

네 설계 의도 감사합니다. ㅎ
모든 원격 호출을 로컬 디스크 쓰기로 저장하다가, 원격 호출이 실패할때는 디스크 읽기를 한다는 말씀이시죠? 캐시레이어에 Disk가 꼭 필요한지도 고민해보시면 좋을 것 같습니다.

DiskLayer는 그런 패턴이 아니라 그냥 일반 캐시 레이어로 동작합니다 — 읽기도 하고 쓰기도 하고, 위 레이어 미스나면 순서대로 접근하는 구조예요. 혼선을 드렸네요.
말씀하신 "원격 호출 결과를 디스크에 저장했다가 실패 시 읽는" 패턴은 사실 stale-if-error 옵션이 더 가깝긴 한데, 그건 메모리에 들고 있는 거라 프로세스 재시작하면 날아가요.
그리고 DiskLayer가 꼭 필요하냐는 지적은 음. 실제로 대부분의 다중 인스턴스 환경에서는 Memory → Redis만으로 충분하고, Disk가 레이어로 들어오는 순간 직렬화 비용이나 파일 관리 복잡도가 따라오거든요.