[아키텍처/소프트웨어] 캐싱

캐싱(caching)이란 데이터 원본을 별도의 저장소에 복제하여 빠르게 제공하는 것이다. 캐싱은 데이터의 응답 지연 시간을 단축시키고 데이터를 제공하는 서버와 데이터를 제공받는 클라이언트 모두의 성능을 향상시킬 수 있는 중요한 방법이다.

원본 데이터를 복제하여 저장하는 저장소를 캐시 저장소(cache store)라고 한다. 서버에 데이터 조회 요청이 들어오면 서버는 캐시 저장소로부터 데이터를 조회한 후 데이터가 있다면 해당 값을 반환하고, 데이터가 없다면 원본 데이터를 조회한 후 캐시 저장소에 저장한다. 캐싱은 별도의 데이터베이스에 존재하는 데이터 원본을 그대로 복제하는 것만을 의미하지는 않는다. 서버가 여러 데이터를 조합하여 새롭게 생성하거나 자체적으로 생성한 데이터를 별도의 저장소에 저장하는 것 모두 캐싱이다.

서버에 데이터 요청이 들어왔을 때 캐시 데이터가 캐시 저장소에 존재하는 경우 캐시 히트(cache hit)(또는 적중)가 발생하며, 캐시 데이터가 캐시 저장소에 존재하지 않는 경우 캐시 미스(cache miss)가 발생한다. 캐시 저장소의 전체 캐시 데이터 중 얼마나 많은 데이터를 대상으로 조회 요청이 유입 되는지에 따라 캐시 히트율과 미스율이 달라지게 된다. 캐시 히트율이 높다는 것은 캐싱의 필요성이 높은 데이터를 저장소에 적절하게 저장했다는 의미이며, 캐시 미스율이 높다는 것은 캐싱의 필요성이 낮은 데이터가 저장소에 저장되어 있다는 의미이다.

캐싱할 데이터를 조회 또는 생성하고 캐시 저장소에 저장하는 것을 캐시 채우기(cache population) 또는 캐시 연산(cache computation)이라고 한다. 캐시 채우기 작업의 예로는 데이터베이스에서 원본 데이터 조회 후 별다른 가공 없이 곧바로 캐시 저장소에 저장, 추가적인 연산 처리 후 재가공하여 캐시 저장소에 저장, 새로운 데이터를 생성하여 캐시 저장소에 저장 등이 있다.

캐시 저장소는 네트워크 상 물리적으로 분리된 별도의 저장소이거나 애플리케이션 내 메모리 공간일 수 있다. 시스템 아키텍처에서 네트워크 상에서 물리적으로 분리된 별도의 캐싱 저장소가 위치한 별도의 레이어(layer)를 캐싱 레이어(caching layer)라고 한다.


로컬 캐싱과 글로벌 캐싱

캐싱은 캐시 데이터를 어느 곳에 위치시킬 것인지에 따라 크게 다음과 같이 두 가지로 구분할 수 있다.

  • 로컬 캐싱 (지역 캐싱)
  • 글로벌 캐싱 (전역 캐싱)


로컬 캐싱(local caching)이란 캐싱을 위해 분리된 별도의 캐싱 레이어 없이 단일 애플리케이션 내에 캐시 데이터를 저장하는 것을 의미한다. 원본 데이터를 데이터베이스에서 조회하고 별도의 처리 없이 그대로 캐싱하거나, 추가적으로 처리하여 특정 형태로 캐싱할 수 있다. 로컬 캐싱을 사용할 경우 캐싱 레이어로의 추가적인 요청이 없으므로 네트워크 트래픽 및 관련 오버헤드가 없으며 따라서 캐시 데이터에 빠르게 접근할 수 있다. 로컬 캐싱의 경우 데이터의 저장 위치 및 데이터의 유효성이 특정 애플리케이션에 한정되므로 시스템이 한 대의 서버로 구성된 경우 또는 데이터 조회 요청이 많지 않거나 데이터의 크기가 상대적으로 작은 경우에 적합하다.

로컬 캐싱은 클라이언트 사이드 캐싱과 서버 사이드 캐싱으로 구분할 수 있다. 클라이언트 사이드 로컬 캐싱의 경우 데이터를 제공 받는 클라이언트가 자주 사용하는 데이터를 로컬 캐싱하여 동일한 데이터에 대한 반복적인 요청을 줄인다. 이를 통해 네트워크 트래픽을 줄이고 내부 처리 속도를 향상시킬 수 있다. 클라이언트 사이드 로컬 캐싱의 예로는 웹 브라우저나 네이티브 모바일 애플리케이션의 정적 파일 캐싱이 있다. 서버 사이드 로컬 캐싱의 경우 데이터를 제공하는 서버가 클라이언트에게 자주 제공하는 응답 데이터를 로컬 캐싱하여 동일한 요청에 대한 응답 데이터를 요청마다 처리하여 제공하는 대신 미리 캐싱된 데이터를 빠르게 제공한다. 이를 통해 서버의 부하를 줄이고 응답 지연 시간을 줄일 수 있다. 서버 사이드 로컬 캐싱의 예로는 백엔드 API 서버의 데이터베이스 데이터 캐싱이 있다. 마이크로서비스 아키텍처 시스템 상에서 한 서버는 또다른 서버에게 데이터를 요청하는 클라이언트가 되기도 하므로 클라이언트 사이드 로컬 캐싱과 서버 사이드 로컬 캐싱 모두 수행 가능하다.

로컬 캐싱은 애플리케이션 내에 캐시 데이터를 저장하는 것이므로 장애 또는 유지보수로 인해 애플리케이션이 재시작 또는 종료되는 경우 캐시 데이터가 삭제되는 단점이 존재한다. 이 경우 클라이언트는 서버에게 새로운 데이터를 요청하고, 서버는 해당 데이터를 다시 캐싱하는 과정이 필요하다. 이 과정에서 서버가 데이터 원본 저장소로 해당 데이터를 조회하는 요청이 갑작스럽게 증가하게 되어 문제가 될 수 있다.

글로벌 캐싱(global caching)이란 캐싱을 위해 분리된 별도의 캐싱 레이어를 사용하여 캐시 데이터를 별도의 서버에 저장하는 것을 의미한다. 이 때, 캐싱 서버는 웹 API 서버 또는 데이터베이스가 될 수 있다. 클라이언트의 입장에서 캐시 데이터를 제공하는 모든 캐싱 서버가 클로벌 캐싱이라고 볼 수 있다. 이때 캐싱 서버는 하나 이상일 수 있다. 캐싱 서버의 개수에 관계 없이 캐시 데이터는 동기화되어 클라이언트에게 데이터 일관성을 제공해야 한다.

로드 밸런서가 클라이언트의 요청을 여러 대의 서버로 분산시키는 분산 시스템(distributed system) 구조에서 클라이언트의 요청 부하는 여러 서버로 균일하게 분산된다. 이때 로드 밸런서가 고정 세션(sticky session) 기능을 제공하지 않는 경우 특정 클라이언트의 요청이 한 서버로만 이루어지는 것이 보장되지 않는다. 따라서 여러 캐싱 서버들로 구성된 분산 시스템 구조에서 서버 간 캐시 데이터가 동기화되지 않은 경우 클라이언트는 데이터의 일관성을 보장 받지 못한다.

글로벌 캐싱을 사용할 경우 클라이언트는 캐싱 레이어로의 추가적인 요청을 수행하게 되어 네트워크 트래픽 및 관련 오버헤드가 발생하므로 로컬 캐싱에 비해 캐시 데이터에 접근하는 속도가 상대적으로 느리다.


캐싱 전략

캐시 데이터를 대상으로 일반적으로 다음과 같은 처리 과정이 이루어진다.

  • 캐시 채우기 (캐시 쓰기)
  • 캐시 만료
  • 캐시 제거
  • 캐시 무효화
  • 캐시 갱신 (캐시 데이터 다시 채우기, 캐시 재계산)


캐시 채우기(population)란 캐시 데이터를 캐시 저장소에 새로 저장하는 것이다. 서버는 캐시 저장소에 캐시 데이터가 있는지 먼저 확인한 후 존재하지 않는다면(캐시 미스가 발생한다면) 캐시 데이터를 원본 저장소에서 조회하거나 내부적으로 원본 데이터를 생성하여 캐시 저장소에 저장한다.

캐시 만료(expiration)란 캐시 데이터가 일정 시간이 지나면 캐시 저장소에서 제거되는 것이다. 캐시 데이터는 만료 기간(TTL, time to live) 특성을 가질 수 있다. 캐시 데이터가 정해진 만료 기간이 지나 만료되면 해당 데이터는 캐시 저장소에서 자동으로 제거된다. 일반적으로 캐시 데이터 만료에 따른 데이터 제거 작업은 저장소에서 자동으로 수행한다. 캐시 데이터에 정해진 만료 기간을 무시하고 새로운 만료 기간을 설정하거나 만료 처리(무효화)할 수 있고, 데이터를 수동으로 제거할 수도 있다. 서버가 만료되었지만 제거되지 않고 캐시 저장소에 남아 있는 캐시 데이터를 조회할 경우 해당 데이터의 만료 여부를 확인하여 적절하게 캐시 채우기 작업을 다시 수행해야 한다.

캐시 제거(eviction)란 한정된 크기의 캐시 저장소를 캐시 데이터 관리를 위해 캐시 데이터를 제거하는 것이다. 캐시 저장소에 저장할 데이터의 크기가 저장소의 최대 크기를 넘어서는 경우 새로운 캐시 데이터를 저장하기 위해 일부 데이터를 제거해야 한다. 캐시 데이터 제거는 캐시 저장소에서 특정 캐시 데이터를 수동으로 제거하는 것, 정해진 규칙에 따라 캐시 데이터를 자동으로 제거하는 것 모두를 의미한다. 일반적으로 캐시 저장소는 정해진 규칙에 따라 데이터를 자동으로 제거함으로써 저장 공간을 관리하며 데이터를 자동으로 제거하는 규칙을 캐시 데이터 제거 정책 또는 제거 알고리즘이라고 한다. 캐시 데이터 제거 정책은 어떤 데이터를 먼저 캐시 저장소에서 제거할지에 대한 규칙이다. 일반적으로 사용되는 정책의 종류는 다음과 같다.

  • LRU (least recently used) 제거: 사용되지 않은 기간이 오래된 데이터 먼저 제거
  • LFU (least frequently used) 제거: 자주 사용되지 않는 데이터 먼저 제거
  • FIFO (first in first out) 제거: 먼저 저장된 데이터를 먼저 제거 (캐시 저장소에 저장된 순서대로 제거)


위 정책을 사용하는 대신 제거 알고리즘을 직접 구현하여 적용할 수도 있다.

캐시 무효화(invalidation)란 캐시 저장소에 저장되어 있는 캐시 데이터의 원본 데이터가 변경되었을 때 더 이상 유효하지 않은 해당 캐시 데이터를 무효 상태로 만드는 것이다. 캐싱에서 유효하지 않은 캐시 데이터란 원본 데이터와 다른(최신화되지 않은) 데이터를 의미한다. 즉, 캐시 데이터에 대한 동일한 요청 파라미터에 대해서 데이터가 변경되어 더 이상 유효하지 않은 데이터를 의미하며, 해당 파라미터로 요청이 더이상 없어 응답할 필요가 없는 캐시 데이터의 경우 데이터의 최신화 여부를 떠나 사용되지 않는 데이터이므로 이 경우 캐시 무효화 처리의 대상이 아닌 캐시 제거 대상에 해당된다.

무효화는 데이터를 최신화하여 일관성을 제공하는 것이 목적이다. 무효화 처리란 해당 캐시 데이터를 무효한 상태로 변경하거나 데이터를 저장소에서 제거하는 것을 말한다. 캐시 데이터를 만료 상태로 변경하는 것은 무효화 처리 방법 중 하나이다. 원본 데이터가 변경되는 주기가 정확하다면 캐시 만료 기간을 설정하면 되므로 별도의 무효화 처리는 필요하지 않다.

무효화 처리 방법은 크게 두 가지이다.

  • 수동 무효화 (명시적 무효화): 원본 데이터를 변경할 때 직접 캐시 데이터를 무효화 처리
  • 자동 무효화 (이벤트 기반 무효화): 원본 데이터의 변경 이벤트를 실시간으로 감지하여 캐시 데이터를 무효화 처리


캐시 데이터가 무효화 처리되면 이후 해당 데이터에 대한 요청에 대해서는 캐시 미스가 발생하며, 유효한 새로운 캐시 데이터의 캐시 채우기 작업이 수행된다.

제거와 무효화의 차이는 다음과 같다.

  • 제거는 캐시 데이터 저장소의 물리적인 캐시 공간 부족 문제를 알고리즘을 통해 논리적으로 해결하는 방법이다. 캐싱 히트율을 높이면서 캐시 저장소의 저장 공간을 관리하는 것이 주 목적이다.
  • 무효화는 유효하지 않은(최신화되지 않은) 캐시 데이터를 제공하는 것을 막음으로써 데이터 일관성 문제를 해결하는 방법이다.

캐시 갱신(refresh)이란 캐시 저장소에 이미 저장되어 있는 캐시 데이터를 최신화하는 것이다. 캐시 데이터 다시 채우기 또는 캐시 재계산(recomputation)이라고도 한다. 캐시 데이터가 무효화 처리되면 서버는 무효화된 캐시 데이터를 최신화된 캐시 데이터로 변경하거나 캐시 저장소에 새로 저장한다.


캐시 무효화

캐시 무효화 처리는 다양한 방법으로 수행된다. 캐싱 기능의 요구사항이나 캐싱 레이어가 시스템적으로 특정 요구사항을 만족해야 할 경우 무효화 처리 메커니즘이 복잡해질 수 있다.

무효화 처리는 새로운 캐시 데이터 채우기 작업(갱신)을 보장하지 않는다. 데이터를 무효 상태로 변경만 수행하고, 신규 데이터로의 변경 또는 신규 데이터 추가 작업은 이후 다른 시점에 수행된다.

앞서 캐시 데이터 무효화 처리 방법은 크게 두 가지로 구분했다.

  • 수동 무효화 (명시적 무효화): 원본 데이터를 변경할 때 직접 캐시 데이터를 무효화 처리
  • 자동 무효화 (이벤트 기반 무효화): 원본 데이터의 변경 이벤트를 실시간으로 감지하여 캐시 데이터를 무효화 처리


수동 무효화 처리는 원본 데이터 저장소의 데이터를 변경하는 작업을 수행할 때 캐시 저장소에 직접 접근하여 캐시 데이터를 무효화 처리하는 작업도 수행한다. 원본 데이터를 변경하는 주체가 캐시 데이터를 직접 명시적으로 무효화 처리한다. 두 작업을 하나의 트랜잭션으로 관리하는 것은 복잡하고 성능 오버헤드가 발생하므로 권장되지 않는다. 수동 무효화 처리는 원본 데이터 변경 시점에 곧바로 캐시 데이터를 무효화 처리하므로 무효화 처리 작업을 쉽고 빠르게 수행하지만, 원본 데이터 변경 로직과 캐시 데이터 무효화 처리 로직 결합된다.

자동 무효화 처리는 원본 데이터를 변경하는 작업만 수행하며, 캐시 저장소에 직접 접근하여 캐시 데이터를 무효화 처리하는 대신, 별도의 캐시 관리 서비스가 원본 데이터 변경을 감지하여 캐시 데이터를 무효화 처리한다. 원본 데이터를 변경하는 주체가 직접 무효화 처리하는 것이 아니라 원본 데이터의 변경 이벤트를 감지하는 다른 주체에 의해 무효화 처리 과정이 이루어진다. 자동 무효화를 구현하는 예는 다음과 같다.

  • 캐시 데이터를 제공하는 서버에서 원본 데이터 변경을 직접적으로 감지:
  • 캐시 데이터를 제공하는 서버에서 원본 데이터 변경을 간접적으로 감지: 데이터베이스의 변경 이벤트를 메시지 큐 시스템으로 관리한다. 원본 데이터가 변경되면 메시지를 발행한다. 별도의 서비스가 해당 메시지를 구독하여 캐시 데이터를 변경한다.


분산 시스템 환경에서 캐싱 무효화 처리는 보다 복잡하다. 다음 분산 시스템 구성에서 캐싱 무효화 처리를 살펴보겠다.

  • 캐싱 레이어: 캐시 저장소를 클러스터 구성
  • 애플리케이션 레이어: 로드 밸런서가 요청을 여러 애플리케이션으로 분산 처리


캐싱 무효화 처리는 캐싱 스탬피드 문제를 발생시킬 수 있다. 대용량의 요청이 유입되는 데이터를 캐싱한 경우 해당 데이터를 무효화 처리하면 동시적인 요청이 캐싱


분산 캐싱

분산 캐싱(distributed caching)이란 네트워크 상에서 서로 분리된 여러 애플리케이션에 캐시 데이터를 저장하고 서로 공유하는 것을 의미한다. 분산 캐싱은 데이터 조회 요청이 많거나 데이터 크기가 큰 경우 캐싱 서버의 부하를 분산시키기도 하지만 서버들을 물리적 및 지리적으로 다르게 위치시켜 클라이언트와 가장 가까운 서버가 보다 낮은 지연 시간 및 빠른 응답 속도로 데이터를 응답하는 장점도 제공한다.

분산 캐싱의 경우 다중 서버가 데이터를 제공하므로 단일 캐싱 서버 보다 더 높은 수준의 시스템의 내결함성, 확장성, 성능을 제공할 수 있다. 분산 시스템에서 분산 캐싱은 데이터의 일관성을 보장해야 한다. 즉, 서로 다른 서버에 대한 요청이 이루어지더라도 클라이언트에게 응답에 대한 일관성을 보장해야 한다.

분산 캐싱 방법 중 하나인 분산 메모리 캐시는 실행 중인 애플리케이션의 메모리에 데이터를 저장한다. 데이터 크기에 따른 메모리 점유 부하 문제가 적은 경우 고려해볼 수 있다.


캐시 스탬피드

서버 애플리케이션이 데이터의 캐싱 처리를 수행하는 경우 서버는 먼저 로컬 캐시 데이터 저장소로부터 데이터를 조회한 후 데이터가 있다면 해당 값을 응답하고, 데이터가 없다면 원본 데이터 저장소(예: 데이터베이스)에서 조회하거나 데이터를 계산 및 생성한 후 로컬 저장소에 저장한다. 요청에 대해 캐싱된 데이터를 응답으로 제공하는 경우 캐시 히트, 캐싱된 데이터가 없어 원본 데이터를 다시 조회하거나 연산 처리하여 캐싱 데이터를 다시 채우는 경우 캐시 미스가 발생한다. 캐싱할 데이터를 조회 또는 생성하고 캐시 저장소에 저장하는 캐시 채우기 작업의 처리 과정이 얼마나 빈번하게 일어나는지는 원본 데이터 저장소, 서버, 캐시 저장소 세 인스턴스의 부하와 관련이 있다.

서버가 동일한 요청을 지속적으로 받는 경우 캐싱 처리는 첫 요청에 대해 한 번만 수행되며 이후 요청에 대해 서버는 캐시 데이터를 응답하므로 요청량이 많더라도 낮은 부하와 빠른 응답 속도로 요청을 처리할 수 있다. 이는 서버 사이드 캐싱 사용의 장점이기도 하다. 이후 다른 새로운 요청이 유입되는 경우에도 해당 요청에 대한 캐싱 처리가 동일하게 수행되고 이후 요청은 모두 캐시 히트를 발생시킨다. 그러나 한 대의 서버가 멀티 스레드를 사용하여 캐시 미스가 발생한 요청에 대한 캐싱 처리를 동시적으로(concurrently) 수행하거나 병렬 연산 시스템에서 여러 서버 애플리케이션(프로세스)이 병렬적으로(parallelly) 수행하는 경우, 스레드(또는 프로세스) 간 캐싱 처리에 대한 적절한 재처리 완화 및 방지 과정이 없다면 동일한 캐시 데이터 채우기 작업이 동시에 반복적으로 일어날 수 있으며 이는 저장소 및 서버에 부하를 발생시킨다. 이러한 환경에서 캐싱을 처리하는데 소요되는 시간 간격 사이에 서버가 받은 요청은 동시적 캐시 데이터 채우기 작업 처리를 유발시키며 이는 시스템의 연쇄적 장애로 이어질 수 있다. 이를 캐시 스탬피드(cache stampede) 또는 캐시 미스 스톰(cache miss storm)이라고 한다. 캐시 스탬피드란 자주 사용되는 캐시 데이터가 캐시 저장소에 존재하지 않는 캐시 미스 상태일 경우 동시에 수많은 요청이 캐시 데이터를 조회하려고 할 때 발생하게 된다. 캐시 스탬피드가 발생하는 시나리오를 정리하면 다음과 같다.

  • 데이터가 캐시에 저장되어 있지 않거나 만료되어 캐시 저장소에서 제거된다.
  • 해당 데이터에 대한 수많은 요청이 동시에 유입된다.
  • 모든 요청은 캐시 저장소에서 해당 데이터를 찾지 못한다(캐시 미스).
  • 서버는 캐시 채우기 작업을 위해 동시에 데이터베이스에서 해당 원본 데이터 조회를 시도한다.
  • 원본 저장소는 단일 데이터를 조회하는 요청을 동시에 처리하게 되고 평상시의 처리량 보다 더 높은 처리량에 대응해야 하므로 부하가 발생한다. 원본 저장소의 데이터 조회 작업이 오래 걸리는 작업일 경우 응답 지연이 발생하게 된다.
  • 원본 저장로부터 조회된 데이터를 다시 캐시 저장소에 저장한다. 캐시 저장소로의 대용량의 쓰기 작업 처리가 동시적으로 발생하게 된다.


캐시 스탬피드 문제는 데이터에 대한 요청 각각이 캐싱 대상 데이터(원본 데이터) 조회 또는 생성 작업, 캐시 저장소로의 저장 작업을 동반하게 된다는 것이다. 이러한 처리 과정 자체가 문제라고 볼 수는 없지만 원본 데이터 저장소, 서버, 캐시 저장소 모두 갑작스런 부하가 발생하게 되므로 부가적인 문제를 일으킬 수 있다. 동시적인 캐시 데이터 채우기 작업을 성능 상의 문제 없이 적절하고 효율적으로 구현하는 것이 필요하다. 이는 동일한 캐시 데이터를 여러 스레드(또는 프로세스)가 저장하려고 하는 동시성 문제이다.

동일한 데이터를 반복적으로 캐싱 처리하는 과정을 방지하기 위한 여러 방법이 있다.

  1. 캐시 데이터 채우기 작업에 대한 동시 처리 제어: 동기화를 통해 하나의 스레드만 캐시 데이터 채우기 작업만을 수행하도록 제어
  2. 캐시 데이터 채우기 작업 수행 횟수의 확률적 감소
  3. 동기적 캐시 데이터 채우기 작업 대신 비동기적 캐시 데이터 채우기 작업


첫 번째 방법은 캐시 데이터 채우기 작업을 수행하는 스레드를 동기화하여 동시 처리를 제어하는 방법이다. 스레드 동기화 기법을 통해 여러 스레드가 캐시 데이터 채우기 작업(데이터 쓰기 작업)을 수행할 수 있도록 하는 대신 별도의 스레드만 수행하도록 한다. 요청에 대한 캐시 미스가 발생하면 스레드는 해당 캐시 데이터 채우기 작업에 대한 락(lock)(쓰기 작업에 대한 락)을 획득하려고 시도한다. 락을 획득한 경우에만 캐시 데이터 채우기 작업을 처리하도록 하여 동시 처리 자체를 제어한다. 캐시 데이터 채우기 작업이 끝나면 해당 작업에 대한 락을 해제한다. 락이 해제될 때까지 다른 스레드가 처리 되는 방법에는 여러 가지가 있다.

  1. 락이 해제될 때까지 대기(블로킹)한다. 캐시 데이터 채우기 작업이 완료되고 락이 해제될 때까지 대기한다. 락이 해제되면 스레드는 캐시 데이터를 다시 확인하고 갱신된 데이터를 조회하여 클라이언트에게 반환한다. 모든 스레드가 최신 데이터를 조회하게 된다는 장점이 있지만 스레드에 대한 요청이 락 해제 대기 시간 만큼 지연되어 동시 처리량이 감소하게 된다.
  2. 락이 해제될 때까지 대기하는 대신 빈 값을 클라이언트에게 정상 응답을 반환하거나, 데이터가 존재하지 않다는 실패 응답을 반환한다. 요청에 대한 응답 지연은 발생하지 않고 동시 처리량이 유지되지만 데이터를 반환하지 못하는 상태가 된다. 동일한 요청에 대해 서로 다른 응답이 반환되므로, 서비스의 데이터 일관성이 감소하게 될 뿐만 아니라 데이터 조회 실패 응답이 반환되므로 가용성도 감소하게 된다.
  3. 락이 해제될 때까지 대기하는 대신 최신화되지 않은 이전의 캐시 데이터를 조회하여 클라이언트에게 반환한다. 요청에 대한 응답 지연은 발생하지 않고 동시 처리량이 유지되지만 동일한 요청에 대해 서로 다른 응답이 반환되므로, 서비스의 데이터 일관성이 감소하게 된다.


두 번째 방법은 하나의 스레드만 캐시 데이터 채우기 작업을 수행하도록 제어하는 대신, 스레드 별로 캐시 데이터 채우기 작업을 수행하도록 하되 작업 수행 횟수를 확률적으로 감소시키기 위해 캐시 데이터를 확률 분포에 기반한 알고리즘으로 만료시키는 것이다. 이러한 방법을 확률적 조기 만료(probabilistic early expiration) 또는 확률적 조기 재계산(probabilistic early recomputation)이라고 한다. 여기서 재계산이란 최신화된 원본 데이터를 조회하거나 새로운 연산을 하여 최신 데이터를 생성하는 것을 말한다. 캐시 데이터의 만료 기간이 임박했을 때, 만료 전에 캐시 데이터를 요청하는 스레드가 별로 독립적으로 난수를 생성하고, 난수를 기반으로 무작위적으로 캐시 데이터를 재계산한다. 만료까지 기간이 많이 남은 데이터일 수록 재계산할 확률이 낮으며, 만료가 임박한 데이터일 수록 재개산할 확률이 높아진다. 이러한 확률적 접근 방식으로 인해 스레드는 동시적으로가 아닌 서로 다른 시간에 캐시 데이터 채우기 작업을 수행하게 되고 데이터 저장소와 서버의 오버헤드가 완화된다. 하지만 재계산 시점과 실제 만료 시점 사이에 원본 데이터가 변경된 경우 실제로는 최신 데이터가 아니지만 만료되지는 않았다고 판단한 캐시 데이터가 제공될 수 있는 가능성이 있다.


캐싱 레이어의 성능


Nginx 캐시 클러스터


바이트 배열 데이터

이미지나 영상 콘텐츠 데이터를 캐싱하는 경우 리소스 출처로부터 제공 받은 이진(binary) 데이터인 바이트 배열 전체를 캐싱하거나 리소스 출처 경로(URL)를 캐싱한다. 바이트 배열을 캐싱하는 경우 클라이언트는 캐싱된 바이트 배열 데이터를 응답으로 받고 미디어 타입에 맞게 변환 처리하여 사용한다.

리소스 출처 경로를 캐싱하는 경우 클라이언트는 응답 받은 해당 URL에 대한 추가 요청을 보낸 후 바이트 배열 데이터를 응답으로 받고 미디어 타입에 맞게 변환 처리한다. 일반적으로 원본 바이트 배열 데이터 보다는 압축이나 크기 및 해상도 변경, 확장자 변경 등에 의해 특정 형태로 변환된 데이터를 캐싱하고 서버의 메모리나 파일 시스템에 저장한다.

웹에서 CORS를 허용하기 위해 이미지나 영상 데이터를 리소스 출처 대신 클라이언트에게 제공하는 프록시 서버의 경우 바이트 배열을 직접 클라이언트에게 응답으로 제공할 수 있다. 이때 바이트 배열 응답 데이터를 캐싱하는 경우 메모리나 파일 저장 공간을 점유하게 되어 리소스 부하가 발생할 수 있으므로 적절한 데이터 변환 과정이 필요하다. 변환된 데이터를 캐싱하거나 적절한 압축 기술을 사용하여 리소스 점유를 최적화할 수 있다.


참고

Comments