지난해 출시 이후에도 Runtime Cache에 꾸준히 투자한 결과, 부하 상황에서도 더 빠르게 동작하고 프로젝트당 저장 용량도 늘었습니다. 관측 기능 역시 지금까지 운영해 온 어떤 서비스보다 한층 강화했습니다.
오늘부터 Data Cache를 Runtime Cache로 통합합니다. 이제 개발자는 프레임워크와 API를 가리지 않고 하나의 제품으로 데이터를 캐싱할 수 있습니다. Next.js fetch 캐싱이나 unstable_cache를 쓰고 있다면 코드를 바꿀 필요가 없습니다. Pro와 Enterprise 프로젝트는 저장 용량도 더 넉넉하게 제공됩니다.
Runtime Cache를 사용하면 별도의 캐싱 서비스를 운영하지 않고도, 외부에서 가져오거나 계산한 데이터를 여러 요청에서 재사용할 수 있습니다. 지난 1년 동안 트래픽이 몰릴 때의 읽기 성능을 개선하고, 저장 용량 한도를 늘렸으며, 캐시 사용 현황을 더 쉽게 살펴볼 수 있도록 했습니다.
Data Cache는 원래 Next.js App Router용 데이터 캐싱 서비스로 출시되어, fetch 캐싱과 unstable_cache에서 사용됐습니다. HTTP 응답 전체를 저장하는 CDN 캐싱과 달리, Data Cache는 응답을 만드는 데 쓰이는 애플리케이션 데이터를 저장합니다. 예를 들어 상품 페이지는 캐시된 카탈로그 데이터를 재사용하면서도 방문자마다 개인화된 응답을 렌더링할 수 있습니다.
저희는 Next.js에 의존하지 않고도 이 서비스를 쓸 수 있기를 바랐습니다. 그래서 지난해 @vercel/functions의 getCache라는 직접 호출용 API와 함께 Runtime Cache를 선보였습니다. 이 API는 Routing Middleware에서 feature flag를 조회하거나, 빌드 중에 CMS 콘텐츠를 캐싱하거나, SvelteKit과 Nuxt 앱에서 데이터를 캐싱하는 등 다양한 용도로 활용할 수 있습니다.
프레임워크가 자체 API를 통해 이 서비스를 제공할 수도 있습니다. Vercel에서는 Next.js도 함수와 컴포넌트의 출력을 캐싱하는 'use cache: remote'에 Runtime Cache API를 사용합니다.
Runtime Cache를 뒷받침하는 인프라는 여러 팀이 함께 쓰는 분산 캐싱 서비스입니다. 요청은 Go API가 처리하고, 그 뒤에서 Vercel의 모든 리전에 배치된 Redis 플릿이 이를 지원합니다. 이 서비스는 매시간 4억 4천만 건이 넘는 읽기 요청을 처리하고 18TB 이상의 데이터를 이동시킵니다.
캐시는 리전마다 따로 있습니다. 캐시 히트는 코드가 실행되는 리전의 캐시 서비스와 한 번만 왕복하면 됩니다. 인증은 Vercel이 수명이 짧은 자격 증명으로 처리하므로 API 키를 직접 관리할 필요가 없습니다.
캐시 미스가 나면 앱이나 프레임워크가 값을 가져오거나 계산해 해당 리전의 캐시에 기록합니다. 이때 다른 리전에 캐시된 사본이 있는지는 확인하지 않습니다.
캐시 항목에는 각각 태그와 기록 시각이 함께 저장됩니다. 서비스는 항목을 읽을 때 같은 리전에 있는 별도의 태그 저장소에서 해당 태그의 무효화 타임스탬프를 확인합니다. 항목이 기록된 뒤에 무효화된 태그가 하나라도 있으면 그 항목은 오래된(stale) 것으로 간주합니다.
예를 들어 애플리케이션이 캐시된 모든 상품 데이터에 catalog 태그를 붙였다고 해 보겠습니다. 카탈로그가 바뀌어 이 태그를 무효화하면, 같은 태그를 쓰는 항목이 백만 개여도 타임스탬프는 단 한 번만 기록됩니다. 해당 항목을 하나하나 찾아서 삭제할 필요가 없습니다.
무효화 요청을 받은 리전의 태그 저장소는 즉시 갱신됩니다. CDN에서도 쓰는 Vercel의 공용 태그 인프라가 글로벌 푸시 파이프라인을 통해 이 타임스탬프를 전파하며, 보통 수백 밀리초 안에 다른 리전에 도달합니다.
무엇을 캐싱할지 정하고 캐시 미스를 처리하는 것은 앱이나 프레임워크의 몫입니다. Runtime Cache는 리전별 저장, 인증, 리전 간 태그 무효화 전파를 맡습니다.
항목이 아직 최신인지 확인하는 과정은 모든 캐시 히트에 의존성을 하나 더합니다. 그래서 트래픽이 몰리는 동안 태그 저장소의 샤드 하나가 느려지면, 캐시된 값이 이미 있어도 읽기가 지연될 수 있었습니다.
이를 해결하려고 서킷 브레이커를 도입했습니다. 서킷 브레이커가 작동하면 읽기 요청은 메모리에 캐시된 태그 타임스탬프로 최신 여부를 임시로 확인하므로, 느린 샤드의 응답을 기다리지 않고 처리됩니다. 이 대체 경로를 사용하는 동안에는 방금 무효화된 데이터를 일시적으로 최신 상태로 판단할 수 있습니다.
서킷 브레이커는 8월에 모든 리전에서 활성화했습니다. 가장 큰 리전에서 트래픽이 가장 심하게 몰린 시점을 비슷한 트래픽 수준에서 비교하면, 캐시 서비스 읽기 지연 시간의 p99가 이전의 약 5분의 1로 줄었습니다. 이 유형의 장애로 울리던 온콜 알림도 더 이상 발생하지 않습니다.
6월에는 Pro 프로젝트, 10월에는 Enterprise 프로젝트의 Runtime Cache 저장 용량 한도를 늘려, 재사용하는 데이터를 더 많이 담을 수 있게 했습니다. 새 데이터를 위해 기존 캐시 항목을 축출(eviction)해야 하는 일이 줄어든 것입니다. 축출이 잦던 Enterprise 프로젝트에서는 하루 만에 캐시 미스율이 대략 절반으로 떨어졌습니다.
Observability의 Runtime Cache 보기에 배포 마커, 재검증 원인, 쓰기 사용률을 추가했습니다. 이를 활용하면 캐시 동작의 변화를 릴리스와 비교하고, 항목이 갱신되는 이유를 파악하고, 캐시에 기록만 되고 거의 읽히지 않는 데이터를 찾아낼 수 있습니다.
Vercel의 캐싱을 Runtime Cache로 일원화해 더 단순하게 만들고 있습니다. 지금까지는 Next.js fetch 캐싱과 unstable_cache가 Data Cache를, getCache와 'use cache: remote'가 Runtime Cache를 사용했습니다. 이번 마이그레이션으로 네 API가 모두 Runtime Cache를 사용하게 되므로, 사용자와 코딩 에이전트는 하나의 저장 용량 한도와 사용량 도구만 다루면 됩니다.
fetch 캐싱이나 unstable_cache를 사용한다면 코드는 그대로 두어도 됩니다. Pro에서는 프로젝트와 환경마다 별도의 저장 용량이 할당되며, 이는 기존 Data Cache에서 팀 전체가 나눠 쓰던 풀보다 8배 큽니다. Enterprise 프로젝트는 기존 Data Cache 저장 용량의 10배를 받습니다.
지금까지 과금하지 않았던 Data Cache의 읽기와 쓰기에는 앞으로 Runtime Cache 요금이 적용됩니다. 신규 팀은 자동으로 Runtime Cache를 사용하고, 기존 팀에는 마이그레이션 및 청구 관련 세부 내용을 사전에 안내해 드립니다.
단계별 적용 일정, 요금, 사전 옵트인 방법은 changelog에서 확인하세요. 마이그레이션 전에 캐시 사용 현황을 점검하고, 최적화 가이드에 따라 불필요한 읽기와 쓰기를 줄여 두시기 바랍니다.