Rails에서의 캐싱: 개요

이 가이드는 캐싱을 통해 Rails 애플리케이션의 성능을 높이는 방법을 소개합니다.

캐싱이란 요청-응답 주기 동안 생성된 콘텐츠를 저장하고 유사한 요청에 재사용하는 것을 의미합니다.

캐싱은 애플리케이션의 성능을 높이는 가장 효과적인 방법 중 하나입니다. 캐싱을 통해 단일 서버와 단일 데이터베이스로 운영되는 웹사이트도 수천 명의 동시 사용자를 지원할 수 있습니다.

Rails는 기본적으로 다양한 캐싱 기능을 제공합니다. 이 가이드에서는 각 기능의 범위와 목적을 설명합니다. 이러한 기술을 마스터하면 과도한 응답 시간이나 서버 비용 없이 수백만 개의 뷰를 제공할 수 있습니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 조각 캐싱과 러시안 인형 캐싱
  • 캐싱 종속성 관리
  • 대체 캐시 저장소
  • 조건부 GET 지원

기본 캐싱

이 섹션에서는 페이지 캐싱, 액션 캐싱, 조각 캐싱 등 3가지 유형의 캐싱 기술을 소개합니다. 기본적으로 Rails는 조각 캐싱을 제공합니다. 페이지 캐싱과 액션 캐싱을 사용하려면 actionpack-page_cachingactionpack-action_caching을 Gemfile에 추가해야 합니다.

기본적으로 캐싱은 프로덕션 환경에서만 활성화됩니다. rails dev:cache를 실행하거나 config/environments/development.rb에서 config.action_controller.perform_cachingtrue로 설정하면 로컬에서 캐싱을 테스트할 수 있습니다.

페이지 캐싱

페이지 캐싱은 Rails 메커니즘으로, 생성된 페이지에 대한 요청을 전체 Rails 스택을 거치지 않고 웹 서버(예: Apache 또는 NGINX)에서 직접 처리할 수 있게 합니다. 이는 매우 빠르지만 모든 상황에 적용할 수는 없습니다(예: 인증이 필요한 페이지). 또한 웹 서버가 파일 시스템에서 직접 파일을 제공하므로 캐시 만료를 구현해야 합니다.

정보: 페이지 캐싱은 Rails 4에서 제거되었습니다. actionpack-page_caching gem을 참고하세요.

액션 캐싱

페이지 캐싱은 before 필터가 있는 액션에 사용할 수 없습니다. 예를 들어 인증이 필요한 페이지에는 사용할 수 없습니다. 이 경우 액션 캐싱을 사용할 수 있습니다. 액션 캐싱은 페이지 캐싱과 유사하지만, 들어오는 웹 요청이 Rails 스택을 거치므로 before 필터를 실행할 수 있습니다. 이를 통해 인증 및 기타 제한 사항을 실행하면서도 캐시된 출력을 제공할 수 있습니다.

정보: 액션 캐싱은 Rails 4에서 제거되었습니다. actionpack-action_caching gem을 참고하세요. DHH의 키 기반 캐시 만료 개요를 참고하세요.

조각 캐싱

동적 웹 애플리케이션은 일반적으로 다양한 구성 요소로 페이지를 구축하며, 이러한 구성 요소는 캐싱 특성이 다릅니다. 페이지의 다른 부분을 별도로 캐싱하고 만료시켜야 할 때 조각 캐싱을 사용할 수 있습니다.

조각 캐싱을 사용하면 뷰 로직의 조각을 캐시 블록으로 감싸고, 다음 요청이 들어올 때 캐시 저장소에서 해당 조각을 제공할 수 있습니다.

예를 들어, 페이지의 각 제품을 캐싱하려면 다음과 같이 사용할 수 있습니다:

<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

애플리케이션이 이 페이지에 대한 첫 번째 요청을 받으면 Rails는 고유한 키로 새 캐시 항목을 작성합니다. 키는 다음과 같은 형태입니다:

views/products/index:bea67108094918eeba42cd4a6e786901/products/1

중간의 문자열은 템플릿 트리 다이제스트입니다. 이는 캐싱 중인 뷰 조각의 내용을 기반으로 계산된 해시 다이제스트입니다. 뷰 조각(예: HTML)이 변경되면 다이제스트가 변경되어 기존 파일이 만료됩니다.

제품 레코드에서 파생된 캐시 버전이 캐시 항목에 저장됩니다. 제품이 수정되면 캐시 버전이 변경되고 이전 버전을 포함하는 모든 캐시된 조각이 무시됩니다.

팁: Memcached와 같은 캐시 저장소는 자동으로 오래된 캐시 파일을 삭제합니다.

특정 조건에서 캐시하려면 cache_if 또는 cache_unless를 사용할 수 있습니다:

<% cache_if admin?, product do %>
  <%= render product %>
<% end %>

컬렉션 캐싱

render 헬퍼는 컬렉션에 대한 개별 템플릿을 캐시할 수 있습니다. 이전 each 예제보다 한 단계 더 나아가 한 번에 모든 캐시 템플릿을 읽을 수 있습니다. 이는 cached: true를 렌더링 컬렉션에 전달하여 수행할 수 있습니다:

<%= render partial: 'products/product', collection: @products, cached: true %>

이전 렌더링에서 캐시된 모든 템플릿이 한 번에 가져와져 훨씬 더 빠르게 실행됩니다. 또한 아직 캐시되지 않은 템플릿은 캐시에 작성되고 다음 렌더링 시 한 번에 가져옵니다.

캐시 키는 구성할 수 있습니다. 아래 예에서는 현재 로케일을 접두사로 사용하여 제품 페이지의 다른 로케일이 서로 덮어쓰지 않도록 합니다:

<%= render partial: 'products/product',
           collection: @products,
           cached: ->(product) { [I18n.locale, product] } %>

러시안 인형 캐싱

캐시된 조각을 다른 캐시된 조각 내부에 중첩할 수 있습니다. 이를 러시안 인형 캐싱이라고 합니다.

러시안 인형 캐싱의 장점은 단일 제품이 업데이트되면 외부 조각을 다시 생성할 때 다른 내부 조각을 재사용할 수 있다는 것입니다.

이전 섹션에서 설명한 것처럼, 캐시된 파일은 캐시된 파일이 직접 의존하는 레코드의 updated_at 값이 변경되면 만료됩니다. 그러나 이렇게 해서는 조각이 중첩된 캐시를 만료시킬 수 없습니다.

예를 들어 다음과 같은 뷰가 있다고 가정합시다:

<% cache product do %>
  <%= render product.games %>
<% end %>

그리고 이 뷰는 다음과 같이 렌더링됩니다:

<% cache game do %>
  <%= render game %>
<% end %>

게임의 어떤 속성이 변경되면 updated_at 값이 현재 시간으로 설정되어 캐시가 만료됩니다. 그러나 제품 객체의 updated_at은 변경되지 않으므로 해당 캐시가 만료되지 않고 애플리케이션이 오래된 데이터를 제공하게 됩니다. 이를 해결하려면 touch 메서드를 사용하여 모델을 연결합니다:

class Product < ApplicationRecord
  has_many :games
end

class Game < ApplicationRecord
  belongs_to :product, touch: true
end

touchtrue로 설정되면 게임 레코드의 updated_at이 변경될 때마다 관련 제품의 updated_at도 변경되어 캐시가 만료됩니다.

공유 부분 캐싱

부분 캐싱을 HTML과 JavaScript 파일 간에 공유할 수 있습니다. 예를 들어 공유 부분 캐싱을 사용하면 템플릿 작성자가 HTML과 JavaScript 파일 간에 부분을 공유할 수 있습니다. 템플릿 리졸버에서 템플릿 파일 경로를 수집할 때 MIME 유형 확장자는 포함되지 않고 템플릿 언어 확장자만 포함됩니다. 따라서 템플릿을 여러 MIME 유형에서 사용할 수 있습니다. HTML과 JavaScript 요청 모두 다음 코드에 응답합니다:

render(partial: 'hotels/hotel', collection: @hotels, cached: true)

이는 hotels/hotel.erb 파일을 로드합니다.

다른 옵션은 부분에 formats 속성을 포함하는 것입니다.

render(partial: 'hotels/hotel', collection: @hotels, formats: :html, cached: true)

이는 MIME 유형에 관계없이 hotels/hotel.html.erb 파일을 로드합니다. 예를 들어 이 부분을 JavaScript 파일에 포함할 수 있습니다.

종속성 관리

캐시를 올바르게 무효화하려면 캐싱 종속성을 적절히 정의해야 합니다. Rails는 일반적인 경우를 처리할 수 있도록 충분히 똑똑하므로 별도로 지정할 필요가 없습니다. 그러나 때로는 사용자 정의 헬퍼를 다룰 때와 같이 명시적으로 정의해야 할 경우가 있습니다.

암시적 종속성

대부분의 템플릿 종속성은 템플릿 자체의 render 호출에서 유추할 수 있습니다. 다음은 ActionView::Digestor가 해석할 수 있는 일부 render 호출 예시입니다:

render partial: "comments/comment", collection: commentable.comments
render "comments/comments"
render 'comments/comments'
render('comments/comments')

render "header" # "comments/header"로 변환

render @topic         # "topics/topic"로 변환
render(topics)         # "topics/topic"로 변환
render(message.topics) # "topics/topic"로 변환

반면에 일부 호출은 캐싱이 제대로 작동하도록 변경해야 합니다. 예를 들어 사용자 정의 컬렉션을 전달하는 경우 다음과 같이 변경해야 합니다:

render @project.documents.where(published: true)
render partial: "documents/document", collection: @project.documents.where(published: true)

명시적 종속성

때로는 유추할 수 없는 템플릿 종속성이 있습니다. 일반적으로 헬퍼에서 렌더링이 발생할네, 번역을 계속하겠습니다.

명시적 종속성

때로는 유추할 수 없는 템플릿 종속성이 있습니다. 일반적으로 헬퍼에서 렌더링이 발생할 때 이런 경우가 발생합니다. 다음은 그 예입니다:

<%= render_sortable_todolists @project.todolists %>

이런 경우 특수한 주석 형식을 사용하여 종속성을 지정해야 합니다:

<%# Template Dependency: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>

단일 테이블 상속 설정과 같은 경우에는 많은 명시적 종속성이 필요할 수 있습니다. 각 템플릿을 나열하는 대신 디렉토리의 모든 템플릿과 일치하는 와일드카드를 사용할 수 있습니다:

<%# Template Dependency: events/* %>
<%= render_categorizable_events @person.events %>

컬렉션 캐싱의 경우, 부분 템플릿에 깨끗한 캐시 호출이 없더라도 템플릿 어디에서든 특수한 주석 형식을 추가하여 여전히 컬렉션 캐싱의 이점을 누릴 수 있습니다:

<%# Template Collection: notification %>
<% my_helper_that_calls_cache(some_arg, notification) do %>
  <%= notification.name %>
<% end %>

외부 종속성

캐시된 블록 내부에서 헬퍼 메서드를 사용하고 나중에 해당 헬퍼를 업데이트하는 경우, 캐시도 업데이트해야 합니다. 어떤 방법으로든 상관없지만 템플릿 파일의 MD5가 변경되어야 합니다. 한 가지 권장 사항은 주석에 명시적으로 표시하는 것입니다:

<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
<%= some_helper_method(person) %>

저수준 캐싱

때로는 뷰 조각 대신 특정 값이나 쿼리 결과를 캐싱해야 할 수 있습니다. Rails의 캐싱 메커니즘은 직렬화할 수 있는 모든 정보를 저장하는 데 매우 효과적입니다.

저수준 캐싱을 가장 효율적으로 구현하는 방법은 Rails.cache.fetch 메서드를 사용하는 것입니다. 이 메서드는 캐시 읽기와 쓰기를 모두 수행합니다. 단일 인수만 전달하면 키가 검색되고 캐시의 값이 반환됩니다. 블록을 전달하면 캐시 누락 시 해당 블록이 실행됩니다. 블록의 반환 값은 지정된 캐시 키 아래에 기록되며 해당 반환 값이 반환됩니다. 캐시 히트 시 블록을 실행하지 않고 캐시된 값이 반환됩니다.

다음 예를 고려해 보겠습니다. 애플리케이션에 경쟁업체 웹사이트의 제품 가격을 조회하는 인스턴스 메서드가 있는 Product 모델이 있습니다. 이 메서드의 반환 데이터는 저수준 캐싱에 완벽합니다:

class Product < ApplicationRecord
  def competing_price
    Rails.cache.fetch("#{cache_key_with_version}/competing_price", expires_in: 12.hours) do
      Competitor::API.find_price(id)
    end
  end
end

참고: 이 예에서는 cache_key_with_version 메서드를 사용했습니다. 이를 통해 생성된 캐시 키는 products/233-20140225082222765838000/competing_price와 같은 형태가 됩니다. cache_key_with_version은 모델의 클래스 이름, id, updated_at 속성을 기반으로 문자열을 생성합니다. 이는 일반적인 관행이며 제품이 업데이트될 때마다 캐시를 무효화하는 이점이 있습니다. 일반적으로 저수준 캐싱을 사용할 때는 캐시 키를 생성해야 합니다.

Active Record 객체 인스턴스 캐싱 피하기

다음과 같은 예를 고려해 보세요. 여기서는 슈퍼 관리자를 나타내는 Active Record 객체의 목록을 캐시에 저장합니다:

# super_admins는 비싼 SQL 쿼리이므로 너무 자주 실행하지 마세요
Rails.cache.fetch("super_admin_users", expires_in: 12.hours) do
  User.super_admins.to_a
end

이 패턴은 피해야 합니다. 왜냐하면 객체 인스턴스가 변경될 수 있기 때문입니다. 프로덕션에서는 속성이 달라질 수 있고 레코드가 삭제될 수 있습니다. 그리고 개발 환경에서는 코드 변경 시 캐시 저장소에 따라 신뢰할 수 없게 작동합니다.

대신 ID나 다른 기본 데이터 유형을 캐싱하는 것이 좋습니다. 예를 들면 다음과 같습니다:

# super_admins는 비싼 SQL 쿼리이므로 너무 자주 실행하지 마세요
ids = Rails.cache.fetch("super_admin_user_ids", expires_in: 12.hours) do
  User.super_admins.pluck(:id)
end
User.where(id: ids).to_a

SQL 캐싱

쿼리 캐싱은 Rails 기능으로, 각 쿼리에 의해 반환된 결과 집합을 캐시합니다. Rails가 동일한 쿼리를 다시 만나면 데이터베이스에 다시 실행하는 대신 캐시된 결과 집합을 사용합니다.

예를 들면 다음과 같습니다:

class ProductsController < ApplicationController
  def index
    # 찾기 쿼리 실행
    @products = Product.all

    # ...

    # 동일한 쿼리 다시 실행
    @products = Product.all
  end
end

동일한 쿼리가 두 번째로 실행될 때는 실제로 데이터베이스에 액세스하지 않습니다. 첫 번째 쿼리 결과가 쿼리 캐시(메모리)에 저장되었다가 두 번째에는 메모리에서 가져옵니다.

그러나 쿼리 캐시는 작업 시작 시 생성되고 작업 종료 시 삭제되므로 작업 기간 동안만 지속된다는 점에 유의해야 합니다. 보다 지속적으로 쿼리 결과를 저장하려면 저수준 캐싱을 사용할 수 있습니다.

캐시 저장소

Rails는 캐시된 데이터를 저장하기 위한 다양한 저장소를 제공합니다(SQL 및 페이지 캐싱 제외).

구성

애플리케이션의 기본 캐시 저장소는 config.cache_store 구성 옵션을 설정하여 설정할 수 있습니다. 다른 매개변수는 캐시 저장소 생성자의 인수로 전달할 수 있습니다:

config.cache_store = :memory_store, { size: 64.megabytes }

또는 구성 블록 외부에서 ActionController::Base.cache_store를 설정할 수 있습니다.

Rails.cache를 호출하여 캐시에 액세스할 수 있습니다.

연결 풀 옵션

기본적으로 :mem_cache_store:redis_cache_store는 연결 풀링을 사용하도록 구성됩니다. 이는 Puma 또는 다른 스레드 서버를 사용하는 경우 여러 스레드가 동시에 캐시 저장소에 쿼리를 수행할 수 있음을 의미합니다.

연결 풀링을 비활성화하려면 캐시 저장소를 구성할 때 :pool 옵션을 false로 설정하세요:

config.cache_store = :mem_cache_store, "cache.example.com", { pool: false }

또한 :pool 옵션에 개별 옵션을 제공하여 기본 풀 설정을 재정의할 수 있습니다:

config.cache_store = :mem_cache_store, "cache.example.com", { pool: { size: 32, timeout: 1 } }
  • :size - 프로세스당 연결 수를 설정합니다(기본값은 5).
  • :timeout - 연결을 대기할 시간(초)을 설정합니다(기본값은 5). 지정된 시간 내에 연결을 사용할 수 없는 경우 Timeout::Error가 발생합니다.

ActiveSupport::Cache::Store

ActiveSupport::Cache::Store은 Rails에서 캐시를 사용하기 위한 기반을 제공합니다. 이는 추상 클래스이므로 단독으로 사용할 수 없습니다. 대신 저장 엔진에 연결된 구체적인 구현을 사용해야 합니다. Rails는 여기에 설명된 여러 구현을 제공합니다.

주요 API 메서드는 read, write, delete, exist?, fetch입니다.

캐시 저장소 생성자에 전달된 옵션은 해당 API 메서드의 기본 옵션으로 처리됩니다.

ActiveSupport::Cache::MemoryStore

ActiveSupport::Cache::MemoryStore은 동일한 Ruby 프로세스 내에서 항목을 메모리에 보관합니다. 캐시 저장소의 크기는 초기화 시 :size 옵션으로 지정할 수 있습니다(기본값은 32MB). 캐시 크기가 허용된 크기를 초과하면 정리가 발생하고 가장 오래된 항목이 제거됩니다.

config.cache_store = :memory_store, { size: 64.megabytes }

여러 Ruby on Rails 서버 프로세스(Phusion Passenger 또는 puma 클러스터 모드를 사용하는 경우)를 실행 중인 경우 Rails 서버 프로세스 인스턴스 간에 캐시 데이터를 공유할 수 없습니다. 이 캐시 저장소는 대규모 애플리케이션 배포에 적합하지 않습니다. 그러나 소규모 저트래픽 사이트와 개발 및 테스트 환경에서는 잘 작동할 수 있습니다.

새로운 Rails 프로젝트는 기본적으로 개발 환경에서 이 구현을 사용하도록 구성됩니다.

참고: :memory_store를 사용하면 프로세스 간에 캐시 데이터를 공유할 수 없으므로 Rails 콘솔에서 캐시를 수동으로 읽기, 쓰기 또는 만료시킬 수 없습니다.

ActiveSupport::Cache::FileStore네, 번역을 계속하겠습니다.

ActiveSupport::Cache::FileStore

ActiveSupport::Cache::FileStore은 파일 시스템을 사용하여 항목을 저장합니다. 저장소 파일이 저장될 디렉토리 경로를 초기화 시 지정해야 합니다.

config.cache_store = :file_store, "/path/to/cache/directory"

이 캐시 저장소를 사용하면 동일한 호스트의 여러 서버 프로세스가 캐시를 공유할 수 있습니다. 이 캐시 저장소는 저트래픽에서 중트래픽 사이트에 적합합니다. 공유 파일 시스템을 사용하여 다른 호스트의 서버 프로세스가 캐시를 공유할 수 있지만 권장되지 않습니다.

디스크가 가득 차면 캐시가 계속 커지므로 주기적으로 오래된 항목을 정리하는 것이 좋습니다.

명시적인 config.cache_store가 제공되지 않으면 이 구현이 기본 캐시 저장소 구현(“#{root}/tmp/cache/”)이 됩니다.

ActiveSupport::Cache::MemCacheStore

ActiveSupport::Cache::MemCacheStore은 Danga의 memcached 서버를 사용하여 애플리케이션에 중앙 집중식 캐시를 제공합니다. Rails는 기본적으로 번들된 dalli gem을 사용합니다. 이는 현재 프로덕션 웹사이트에서 가장 널리 사용되는 캐시 저장소입니다. 매우 높은 성능과 중복성을 제공하는 단일 공유 캐시 클러스터를 제공할 수 있습니다.

캐시를 초기화할 때 클러스터의 모든 memcached 서버 주소를 지정해야 하거나 MEMCACHE_SERVERS 환경 변수가 적절하게 설정되어 있어야 합니다.

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

둘 다 지정되지 않으면 memcached가 localhost의 기본 포트(127.0.0.1:11211)에서 실행 중이라고 가정하지만, 이는 대규모 사이트에 적합하지 않습니다.

config.cache_store = :mem_cache_store # $MEMCACHE_SERVERS 또는 127.0.0.1:11211로 폴백

지원되는 주소 유형은 Dalli::Client 문서를 참조하세요.

이 캐시의 write(및 fetch) 메서드는 memcached의 특정 기능을 활용할 수 있는 추가 옵션을 허용합니다.

ActiveSupport::Cache::RedisCacheStore

ActiveSupport::Cache::RedisCacheStore은 Redis의 자동 만료 기능을 활용하여 Memcached 캐시 서버와 유사하게 동작할 수 있습니다.

배포 참고: Redis는 기본적으로 키를 만료시키지 않으므로 전용 Redis 캐시 서버를 사용해야 합니다. 영구 Redis 서버에 휘발성 캐시 데이터를 채우지 마세요! Redis 캐시 서버 설정 가이드를 자세히 읽으세요.

캐시 전용 Redis 서버의 경우 maxmemory-policyallkeys 변형 중 하나로 설정하세요. Redis 4+는 least-frequently-used 제거(allkeys-lfu)를 지원하며, 이는 훌륭한 기본 선택입니다. Redis 3 이하에서는 least-recently-used 제거(allkeys-lru)를 사용해야 합니다.

캐시 읽기 및 쓰기 시간 초과를 상대적으로 짧게 설정하세요. 캐시된 값을 다시 생성하는 것이 1초 이상 기다리는 것보다 종종 더 빠릅니다. 기본적으로 읽기와 쓰기 시간 초과는 1초이지만 네트워크가 일관되게 지연 시간이 낮은 경우 더 낮출 수 있습니다.

기본적으로 캐시 저장소는 요청 중 한 번 Redis 연결이 실패하면 다시 연결을 시도합니다.

캐시 읽기 및 쓰기는 예외를 발생시키지 않고 대신 nil을 반환합니다. 즉, 캐시에 아무것도 없는 것처럼 동작합니다. 캐시에 예외가 발생하는지 확인하려면 예외 수집 서비스에 보고할 error_handler를 제공할 수 있습니다. 이 핸들러는 method, 사용자에게 반환된 값(nil이 일반적임), exception 등 3개의 키워드 인수를 받아야 합니다.

시작하려면 Gemfile에 redis gem을 추가하세요:

gem "redis"

마지막으로 관련 config/environments/*.rb 파일에 구성을 추가하세요:

config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }

더 복잡한 프로덕션 Redis 캐시 저장소는 다음과 같을 수 있습니다:

cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: cache_servers,

  connect_timeout:    30,  # 기본값은 1초
  read_timeout:       0.2, # 기본값은 1초
  write_timeout:      0.2, # 기본값은 1초
  reconnect_attempts: 2,   # 기본값은 1

  error_handler: -> (method:, returning:, exception:) {
    # 예외를 Sentry에 경고 수준으로 보고
    Sentry.capture_exception exception, level: 'warning',
      tags: { method: method, returning: returning }
  }
}

ActiveSupport::Cache::NullStore

ActiveSupport::Cache::NullStore은 각 웹 요청의 범위 내에서 작동하며 요청 종료 시 저장된 값을 지웁니다. 개발 및 테스트 환경에서 사용하기 위한 것입니다. Rails.cache와 직접 상호 작용하는 코드가 있지만 캐싱이 코드 변경 결과를 보는 데 방해가 되는 경우 매우 유용할 수 있습니다.

config.cache_store = :null_store

사용자 정의 캐시 저장소

ActiveSupport::Cache::Store를 확장하고 적절한 메서드를 구현하여 사용자 정의 캐시 저장소를 만들 수 있습니다. 이를 통해 Rails 애플리케이션에 다양한 캐싱 기술을 통합할 수 있습니다.

사용자 정의 캐시 저장소를 사용하려면 단순히 새 인스턴스를 캐시 저장소로 설정하면 됩니다.

config.cache_store = MyCacheStore.new

캐시 키

캐시에 사용되는 키는 cache_key 또는 to_param에 응답하는 모든 객체가 될 수 있습니다. 필요한 경우 클래스에 cache_key 메서드를 구현하여 사용자 정의 키를 생성할 수 있습니다. Active Record는 클래스 이름과 레코드 ID를 기반으로 키를 생성합니다.

해시와 값 배열을 캐시 키로 사용할 수 있습니다.

# 이는 유효한 캐시 키입니다
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])

Rails.cache에 사용하는 키는 실제 저장 엔진에서 사용되는 키와 동일하지 않습니다. 네임스페이스로 수정되거나 기술 백엔드 제약 사항에 맞게 변경될 수 있습니다. 즉, Rails.cache에 값을 저장했다고 해서 dalli gem으로 가져올 수는 없습니다. 그러나 memcached 크기 제한을 초과하거나 구문 규칙을 위반할 걱정은 할 필요가 없습니다.

조건부 GET 지원

조건부 GET은 HTTP 사양의 기능으로, 웹 서버가 브라우저에 요청 내용이 마지막 요청 이후 변경되지 않았음을 알려줄 수 있는 방법을 제공합니다. 이를 통해 브라우저는 캐시된 버전을 안전하게 사용할 수 있습니다.

이는 HTTP_IF_NONE_MATCHHTTP_IF_MODIFIED_SINCE 헤더를 사용하여 고유한 콘텐츠 식별자와 마지막 변경 시간을 전달하고 받는 방식으로 작동합니다. 브라우저의 요청에서 서버의 버전과 ETag 또는 마지막 수정 시간이 일치하면 서버는 수정되지 않았음을 나타내는 빈 응답만 보내면 됩니다.

서버(즉, 우리)의 책임은 마지막 수정 타임스탬프와 if-none-match 헤더를 찾아 응답을 보내야 할지 결정하는 것입니다. Rails의 조건부 GET 지원을 통해 이 작업은 매우 쉽습니다:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    # 타임스탬프와 etag 값이 일치하지 않으면 이 블록을 실행
    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
      respond_to do |wants|
        # ... 일반적인 응답 처리
      end
    end

    # 요청이 신선하면(수정되지 않음) 아무 작업도 할 필요가 없습니다.
    # 이전 stale? 호출에서 사용한 매개변수를 기반으로 기본 렌더링이 자동으로 수행됩니다.
  end
end

옵션 해시 대신 모델을 전달할 수도 있습니다. Rails는 updated_atcache_key_with_version 메서드를 사용하여 last_modifiedetag를 설정합니다:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    if stale?(@product)
      respond_to do |wants|
        # ... 일반적인 응답 처리
      end
    end
  end
end

특별한 응답 처리를 하지 않고 기본 렌더링 메커니즘(즉, respond_to나 직접 렌더링 호출을 사용하지 않는 경우)을 사용하는 경우 fresh_when 헬퍼를 사용할 수 있습니다:

class ProductsController < ApplicationController
  # 요청이 신선하면 자동으로 :not_modified를 보내고,
  # 요청이 신선하지 않으면 기본 템플릿(product.*)을 렌더링합니다.

  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, etag: @product
  end
end

때로는 정적 페이지와 같이 만료되지 않는 응답을 캐시하고 싶을 수 있습니다. 이를 위해 http_cache_forever 헬퍼를 사용할 수 있으며, 이를 통해 브라우저와 프록시가네, 번역을 계속하겠습니다.

때로는 정적 페이지와 같이 만료되지 않는 응답을 캐시하고 싶을 수 있습니다. 이를 위해 http_cache_forever 헬퍼를 사용할 수 있으며, 이를 통해 브라우저와 프록시가 무기한 캐시할 수 있습니다.

기본적으로 캐시된 응답은 비공개이며 사용자의 웹 브라우저에만 캐시됩니다. 프록시가 모든 사용자에게 캐시된 응답을 제공할 수 있도록 하려면 public: true를 설정하세요.

이 헬퍼를 사용하면 last_modified 헤더가 Time.new(2011, 1, 1).utc로 설정되고 expires 헤더가 100년으로 설정됩니다.

경고: 이 방법을 주의해서 사용하세요. 브라우저/프록시가 강제로 캐시를 지우지 않는 한 캐시된 응답을 무효화할 수 없습니다.

class HomeController < ApplicationController
  def index
    http_cache_forever(public: true) do
      render
    end
  end
end

강한 ETag 대 약한 ETag

Rails는 기본적으로 약한 ETag를 생성합니다. 약한 ETag를 사용하면 의미적으로 동등한 응답이 정확히 일치하지 않더라도 동일한 ETag를 가질 수 있습니다. 이는 응답 본문의 사소한 변경으로 인해 페이지가 다시 생성되는 것을 방지하는 데 유용합니다.

약한 ETag는 W/로 시작하여 강한 ETag와 구분됩니다.

W/"618bbc92e2d35ea1945008b42799b0e7" → 약한 ETag
"618bbc92e2d35ea1945008b42799b0e7" → 강한 ETag

약한 ETag와 달리 강한 ETag는 응답이 정확히 동일하고 바이트 단위로 일치해야 함을 의미합니다. Akamai와 같은 일부 CDN은 강한 ETag만 지원합니다. 절대적으로 강한 ETag를 생성해야 하는 경우 다음과 같이 할 수 있습니다.

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
  end
end

또한 응답에 강한 ETag를 직접 설정할 수 있습니다.

response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"

개발 환경에서의 캐싱

애플리케이션의 캐싱 전략을 테스트하고 싶은 경우가 많습니다. Rails는 rails dev:cache 명령을 제공하여 개발 모드에서 캐싱을 쉽게 켜고 끌 수 있습니다.

$ bin/rails dev:cache
Development mode is now being cached.
$ bin/rails dev:cache
Development mode is no longer being cached.

기본적으로 개발 모드 캐싱이 꺼져 있을 때 Rails는 :null_store를 사용합니다.

참고 자료