rubyonrails.org에서 더 보기:

이 파일은 GITHUB에서 읽지 마시고, https://guides.rubyonrails.org 에서 확인하세요.

Rails에서의 캐싱: 개요

이 가이드는 캐싱을 통해 Rails 애플리케이션의 속도를 향상시키는 방법을 소개합니다.

캐싱은 요청-응답 사이클에서 생성된 콘텐츠를 저장하고 유사한 요청에 대응할 때 재사용하는 것을 의미합니다.

캐싱은 애플리케이션의 성능을 향상시키는 가장 효과적인 방법인 경우가 많습니다. 캐싱을 통해, 단일 서버와 단일 데이터베이스로 실행되는 웹사이트도 수천 명의 동시 사용자 부하를 견딜 수 있습니다.

Rails는 기본적으로 캐싱 기능들을 제공합니다. 이 가이드는 각 기능들의 범위와 목적을 알려드릴 것입니다. 이러한 기법들을 마스터하면 Rails 애플리케이션이 과도한 응답 시간이나 서버 비용 없이 수백만 건의 뷰를 제공할 수 있습니다.

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

  • Fragment와 Russian doll 캐싱
  • 캐싱 의존성 관리 방법
  • 대체 캐시 저장소
  • Conditional GET 지원

1 기본 캐싱

여기서는 세 가지 캐싱 기법을 소개합니다: page, action 그리고 fragment 캐싱입니다. Rails는 기본적으로 fragment 캐싱을 제공합니다. page와 action 캐싱을 사용하려면 Gemfileactionpack-page_cachingactionpack-action_caching을 추가해야 합니다.

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

주의: config.action_controller.perform_caching 값을 변경하는 것은 Action Controller가 제공하는 캐싱에만 영향을 미칩니다. 예를 들어, 아래에서 다룰 low-level 캐싱에는 영향을 주지 않습니다.

1.1 Page Caching

Page caching은 생성된 페이지에 대한 요청이 전체 Rails 스택을 거치지 않고 웹 서버(즉, Apache나 NGINX)에 의해 처리될 수 있도록 하는 Rails 메커니즘입니다. 이는 매우 빠르지만 모든 상황(인증이 필요한 페이지와 같은)에 적용할 수는 없습니다. 또한, 웹 서버가 파일시스템에서 직접 파일을 제공하기 때문에 캐시 만료를 구현해야 할 필요가 있습니다.

Page Caching은 Rails 4에서 제거되었습니다. actionpack-page_caching gem을 참조하세요.

1.2 Action Caching

Page Caching은 before filter가 있는 action에서는 사용할 수 없습니다 - 예를 들어, 인증이 필요한 페이지들입니다. 이럴 때 Action Caching이 필요합니다. Action Caching은 Page Caching과 비슷하게 작동하지만 들어오는 웹 요청이 Rails 스택을 거치므로 캐시가 제공되기 전에 before filter가 실행될 수 있습니다. 이를 통해 캐시된 복사본의 출력 결과를 제공하면서도 인증 및 기타 제한 사항을 실행할 수 있습니다.

Action Caching은 Rails 4에서 제거되었습니다. actionpack-action_caching gem을 참조하세요. 새롭게 선호되는 방식은 DHH의 key-based cache expiration 개요를 참조하세요.

1.3 Fragment Caching

동적 웹 애플리케이션은 일반적으로 서로 다른 캐싱 특성을 가진 다양한 컴포넌트들로 페이지를 구성합니다. 페이지의 각기 다른 부분들을 별도로 캐시하고 만료시켜야 할 때는 Fragment Caching을 사용할 수 있습니다.

Fragment Caching을 사용하면 view 로직의 일부를 캐시 블록으로 감싸서, 다음 요청이 들어올 때 캐시 저장소에서 바로 제공할 수 있습니다.

예를 들어, 페이지의 각 제품을 캐시하고 싶다면 다음과 같은 코드를 사용할 수 있습니다:

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

애플리케이션이 이 페이지에 대한 첫 번째 request를 받으면, Rails는 고유한 key를 가진 새로운 cache entry를 작성합니다. key는 다음과 같은 형태를 가집니다:

views/products/index:bea67108094918eeba42cd4a6e786901/products/1

중간의 문자열은 template tree digest입니다. 이는 캐싱하는 view 조각의 내용을 기반으로 계산된 해시 다이제스트입니다. view 조각을 변경하면(예: HTML이 변경됨) digest도 변경되어 기존 파일이 만료됩니다.

product 레코드로부터 생성된 캐시 버전이 cache entry에 저장됩니다. product가 touch 될 때 캐시 버전이 변경되고, 이전 버전을 포함하는 모든 캐시된 조각들은 무시됩니다.

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

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

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

이는 admin?이 참일 때만 캐싱을 수행하는데, admin?이 거짓이면 일반적인 uncached render가 수행됩니다.

1.3.1 Collection Caching

render 헬퍼는 컬렉션을 위해 렌더링된 개별 템플릿들도 캐시할 수 있습니다. 심지어 each를 사용한 이전 예제보다 한 단계 더 나아가서 캐시 템플릿을 하나씩이 아닌 한 번에 모두 읽을 수 있습니다. 이는 컬렉션을 렌더링할 때 cached: true를 전달하여 수행됩니다:

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

이 코드는 products/product partial을 @products collection에 대해 렌더링하며, 캐싱이 활성화되어 있습니다.

이전 렌더링의 모든 캐시된 템플릿들이 훨씬 더 빠른 속도로 한 번에 가져와집니다. 또한, 아직 캐시되지 않은 템플릿들은 캐시에 기록되어 다음 렌더링에서 한꺼번에 가져올 수 있습니다.

캐시 키는 설정이 가능합니다. 아래 예제에서는 현재 locale이 접두사로 붙어있어서 제품 페이지의 서로 다른 지역화 버전들이 서로 덮어쓰지 않도록 보장합니다:

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

1.4 Russian Doll Caching

중첩된 캐시 조각을 다른 캐시 조각 내에 넣고 싶을 수 있습니다. 이를 Russian doll caching이라고 합니다.

Russian doll caching의 장점은 단일 product가 업데이트되었을 때, 외부 조각을 재생성하는 동안 다른 모든 내부 조각들을 재사용할 수 있다는 것입니다.

이전 섹션에서 설명한 것처럼, 캐시된 파일은 해당 파일이 직접적으로 의존하는 record의 updated_at 값이 변경되면 만료됩니다. 하지만, 이것은 해당 조각이 중첩되어 있는 어떤 캐시도 만료시키지 않습니다.

예를 들어, 다음과 같은 view를 보겠습니다:

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

이는 다음 view를 렌더링합니다:

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

게임의 어떤 속성이 변경되면 updated_at 값이 현재 시간으로 설정되어 캐시가 만료됩니다. 하지만 product 객체의 updated_at은 변경되지 않기 때문에 해당 캐시는 만료되지 않고 앱은 오래된 데이터를 제공하게 됩니다. 이를 해결하기 위해 touch 메서드를 사용하여 모델들을 연결합니다:

class Product < ApplicationRecord
  has_many :games
end

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

touchtrue로 설정하면, game 레코드의 updated_at을 변경하는 모든 액션이 연결된 product의 updated_at도 함께 변경하게 되어 캐시가 만료됩니다.

1.5 Shared Partial Caching

서로 다른 MIME type을 가진 파일들 간에 partial과 그에 연관된 caching을 공유하는 것이 가능합니다. 예를 들어, shared partial caching을 사용하면 템플릿 작성자가 HTML과 JavaScript 파일 간에 partial을 공유할 수 있습니다. 템플릿이 template resolver 파일 경로에서 수집될 때, MIME type이 아닌 템플릿 언어 확장자만 포함됩니다. 이러한 이유로 템플릿은 여러 MIME type에 사용될 수 있습니다. HTML과 JavaScript 요청 모두 다음 코드에 응답할 것입니다:

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

collection rendering에서 caching을 켜면 cached: true입니다. 단일 partial rendering에서와 같이 개별 template에 캐시 설정이 포함되어 있지 않더라도 캐싱이 가능합니다.

hotels/hotel.erb 파일을 불러올 것입니다.

다른 방법으로는 렌더링할 partial에 formats attribute를 포함하는 것입니다.

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

partial을 기반으로 HTML 형식의 view를 생성합니다. @hotels 컬렉션의 각 멤버에 대해 partial이 렌더링되며, 결과는 캐시됩니다.

어떤 MIME type 파일에서든 hotels/hotel.html.erb라는 이름의 파일을 로드합니다. 예를 들어 JavaScript 파일 안에 이 partial을 포함할 수 있습니다.

1.6 의존성 관리

캐시를 올바르게 무효화하기 위해서는 캐싱 의존성을 적절하게 정의해야 합니다. Rails는 일반적인 경우를 처리할 만큼 충분히 똑똑하기 때문에 별도로 지정할 필요가 없습니다. 하지만 때때로 custom helper를 다루는 경우와 같이 명시적으로 정의해야 하는 경우가 있습니다.

1.6.1 암시적 의존성

대부분의 template 의존성은 template 자체의 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"으로 변환됨

반면에 캐싱이 제대로 작동하게 하려면 일부 호출을 변경해야 합니다. 예를 들어, custom collection을 전달하는 경우에는 다음과 같이 변경해야 합니다:

render @project.documents.where(published: true)
```의 형태로 렌더링할 경우, published가 true인 각 document에 대해 _document.html.erb 파셜이 렌더링됩니다.

죄송하지만 번역할 텍스트가 보이지 않습니다. 한국어로 번역을 하려면 영어 원문이 필요합니다. 번역하고 싶은 Rails 가이드 문서를 공유해 주시면 도와드리겠습니다.

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

published 상태가 true인 프로젝트의 documents를 collection으로 하여 "documents/document" partial을 렌더링합니다.

1.6.2 명시적 의존성

템플릿 의존성을 전혀 유추할 수 없는 경우가 있습니다. 이는 일반적으로 helper에서 rendering이 발생하는 경우입니다. 다음은 그 예시입니다:

<%= render_sortable_todolists @project.todolists %>

이 형태로 유지되었습니다. 기술적인 문법 요소이므로 번역하지 않았습니다.

이러한 것들을 표시하기 위해서는 특별한 주석 형식을 사용해야 합니다:

<%# 템플릿 의존성: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>

단일 테이블 상속(single table inheritance) 설정과 같은 일부 경우에서는 다수의 명시적인 dependency를 가질 수 있습니다. 모든 template을 일일이 작성하는 대신, 와일드카드를 사용하여 디렉토리 내의 모든 template을 매칭할 수 있습니다:

<%# 템플릿 의존성: events/* %>
<%= render_categorizable_events @person.events %>

컬렉션 캐싱의 경우, partial 템플릿이 clean cache 호출로 시작하지 않더라도 템플릿 어디에든 특별한 comment 포맷을 추가하여 컬렉션 캐싱의 이점을 얻을 수 있습니다:

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

1.6.3 외부 의존성

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

<%# Helper Dependency Updated: 2015년 7월 28일 오후 7시 %>
<%= some_helper_method(person) %>

1.7 Low-Level Caching

때로는 view 프래그먼트를 캐싱하는 대신 특정 값이나 쿼리 결과를 캐싱해야 할 필요가 있습니다. Rails의 캐싱 메커니즘은 직렬화 가능한 모든 정보를 저장하는데 탁월합니다.

low-level 캐싱을 구현하는 가장 효율적인 방법은 Rails.cache.fetch 메서드를 사용하는 것입니다. 이 메서드는 캐시에 대한 읽기와 쓰기를 모두 수행합니다. 단일 인자만 전달되면 키를 가져와서 캐시의 값을 반환합니다. 블록이 전달되면 캐시 miss가 발생했을 때 해당 블록이 실행됩니다. 블록의 반환값은 주어진 캐시 키로 캐시에 작성되며, 그 반환값이 반환됩니다. 캐시 hit의 경우, 블록을 실행하지 않고 캐시된 값이 반환됩니다.

다음 예시를 살펴보세요. 애플리케이션에 Product 모델이 있고, 경쟁사 웹사이트에서 제품의 가격을 조회하는 인스턴스 메서드가 있습니다. 이 메서드가 반환하는 데이터는 low-level 캐싱에 완벽한 사례가 됩니다:

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

위 예시에서는 경쟁사의 가격을 찾아보는 외부 API를 호출하는 competing_price 메서드가 있습니다. 요청마다 API를 호출하는 것을 방지하기 위해, 이 메서드는 API 응답을 12시간 동안 캐시하고 있습니다. 여기서 캐시 key는 모델과 연관된 cache_key_with_version 메서드를 이용해 생성됩니다.

이 예시에서 cache_key_with_version 메서드를 사용했다는 점에 주목하세요. 그래서 생성되는 cache key는 products/233-20140225082222765838000/competing_price와 같은 형태가 됩니다. cache_key_with_version은 모델의 클래스 이름, id, updated_at 속성을 기반으로 문자열을 생성합니다. 이는 일반적인 관례이며 product가 업데이트될 때마다 cache를 무효화한다는 장점이 있습니다. 일반적으로 low-level caching을 사용할 때는 cache key를 생성해야 합니다.

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

다음 예시를 보세요. superuser들을 나타내는 Active Record 객체 목록을 cache에 저장합니다:

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

이러한 패턴은 피해야 합니다. 왜일까요? instance가 변경될 수 있기 때문입니다. production 환경에서는 attribute가 달라질 수 있거나 record가 삭제될 수 있습니다. 그리고 개발 환경에서는 코드를 변경할 때 reload하는 cache store에서 신뢰성 있게 동작하지 않습니다.

대신, ID나 다른 primitive data type을 캐시하세요. 예를 들면:

# 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

1.8 SQL 캐싱

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

예를 들어:

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

    # ...

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

같은 쿼리가 데이터베이스에 대해 두 번째로 실행될 때는 실제로 데이터베이스에 접근하지 않습니다. 쿼리 결과가 처음 반환될 때 query cache(메모리)에 저장되고 두 번째는 메모리에서 가져옵니다.

하지만 query cache는 action이 시작될 때 생성되고 action이 종료될 때 제거되어 해당 action 기간 동안만 유지된다는 점에 주의해야 합니다. 쿼리 결과를 더 지속적으로 저장하고 싶다면 low-level caching을 사용할 수 있습니다.

2 Cache Stores

Rails는 (SQL과 page caching 외에) 캐시된 데이터를 위한 다양한 store를 제공합니다.

2.1 설정

config.cache_store 설정 옵션을 통해 애플리케이션의 기본 cache store를 설정할 수 있습니다. 다른 매개변수들은 cache store의 생성자에 인자로 전달할 수 있습니다:

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

메모리 저장소 크기는 64메가바이트로 제한됩니다.

다른 방법으로는 configuration 블록 외부에서 ActionController::Base.cache_store를 설정할 수 있습니다.

Rails.cache를 호출하여 캐시에 접근할 수 있습니다.

2.1.1 Connection Pool Options

기본적으로 :mem_cache_store:redis_cache_store는 connection pooling을 사용하도록 구성되어 있습니다. 이는 Puma나 다른 스레드 서버를 사용하는 경우 여러 스레드가 동시에 캐시 저장소에 쿼리를 수행할 수 있다는 것을 의미합니다.

connection pooling을 비활성화하려면 캐시 저장소를 구성할 때 :pool 옵션을 false로 설정하면 됩니다:

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

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

config.cache_store = :mem_cache_store, "cache.example.com", { pool: { size: 32, timeout: 1 } }
  • :size - 이 옵션은 프로세스당 connection 수를 설정합니다(기본값 5).

  • :timeout - 이 옵션은 connection을 기다리는 시간(초)을 설정합니다(기본값 5). timeout 시간 내에 사용 가능한 connection이 없으면 Timeout::Error가 발생합니다.

2.2 ActiveSupport::Cache::Store

ActiveSupport::Cache::Store는 Rails에서 캐시와 상호작용하기 위한 기반을 제공합니다. 이것은 추상 클래스이며, 단독으로는 사용할 수 없습니다. 대신 저장소 엔진과 연결된 구체적인 구현체를 사용해야 합니다. Rails는 아래에 문서화된 여러 구현체들과 함께 제공됩니다.

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

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

2.3 ActiveSupport::Cache::MemoryStore

ActiveSupport::Cache::MemoryStore는 동일한 Ruby 프로세스의 메모리에 엔트리를 유지합니다. cache store는 initializer에 :size 옵션을 전달하여 지정된 제한된 크기를 가집니다(기본값은 32Mb). cache가 할당된 크기를 초과하면 정리가 수행되고 가장 최근에 사용되지 않은 엔트리들이 제거됩니다.

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

여러 Ruby on Rails 서버 프로세스를 실행하고 있다면(Phusion Passenger나 puma clustered mode를 사용하는 경우), Rails 서버 프로세스 인스턴스들이 서로 캐시 데이터를 공유할 수 없습니다. 이 cache store는 대규모 애플리케이션 배포에는 적합하지 않습니다. 하지만 소규모이면서 트래픽이 적고 서버 프로세스가 몇 개 없는 사이트나 개발 및 테스트 환경에서는 잘 작동할 수 있습니다.

새로운 Rails 프로젝트는 기본적으로 개발 환경에서 이 구현을 사용하도록 설정되어 있습니다.

:memory_store를 사용할 때는 프로세스 간에 캐시 데이터를 공유하지 않기 때문에, Rails 콘솔을 통해 수동으로 캐시를 읽거나 쓰거나 만료시키는 것이 불가능합니다.

2.4 ActiveSupport::Cache::FileStore

ActiveSupport::Cache::FileStore는 entry를 저장하기 위해 파일 시스템을 사용합니다. cache를 초기화할 때 저장소 파일이 저장될 디렉토리 경로를 지정해야 합니다.

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

이 cache store를 사용하면 동일한 호스트의 여러 서버 프로세스가 cache를 공유할 수 있습니다. 이 cache store는 하나 또는 두 개의 호스트에서 서비스되는 낮거나 중간 정도의 트래픽을 가진 사이트에 적합합니다. 서로 다른 호스트에서 실행되는 서버 프로세스들은 공유 파일 시스템을 사용하여 cache를 공유할 수 있지만, 이러한 설정은 권장되지 않습니다.

cache가 디스크가 가득 찰 때까지 증가하므로, 주기적으로 오래된 항목들을 정리하는 것이 권장됩니다.

명시적인 config.cache_store가 제공되지 않은 경우 이것이 기본 cache store 구현("#{root}/tmp/cache/")입니다.

2.5 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의 특정 기능을 활용하는 추가 옵션을 허용합니다.

2.6 ActiveSupport::Cache::RedisCacheStore

ActiveSupport::Cache::RedisCacheStore는 Redis의 최대 메모리 도달 시 자동 제거 기능을 활용하여 Memcached 캐시 서버와 매우 유사하게 동작할 수 있습니다.

배포 시 참고사항: Redis는 기본적으로 키를 만료시키지 않으므로, 전용 Redis 캐시 서버를 사용하도록 주의하세요. 영구 Redis 서버를 휘발성 캐시 데이터로 채우지 마세요! Redis 캐시 서버 설정 가이드를 자세히 읽어보세요.

캐시 전용 Redis 서버의 경우, maxmemory-policy를 allkeys 변형 중 하나로 설정하세요. Redis 4+ 버전은 최소 사용 빈도 제거(allkeys-lfu)를 지원하며, 이는 탁월한 기본 선택입니다. Redis 3 이하 버전은 최소 최근 사용 제거(allkeys-lru)를 사용해야 합니다.

캐시 읽기 및 쓰기 타임아웃을 상대적으로 낮게 설정하세요. 캐시된 값을 재생성하는 것이 1초 이상 기다리는 것보다 더 빠른 경우가 많습니다. 읽기와 쓰기 타임아웃은 기본적으로 1초로 설정되어 있지만, 네트워크가 일관되게 낮은 지연 시간을 보인다면 더 낮게 설정할 수 있습니다.

기본적으로 캐시 스토어는 요청 중 연결이 실패할 경우 한 번 재연결을 시도합니다.

캐시 읽기와 쓰기는 절대 예외를 발생시키지 않습니다. 대신 캐시에 아무것도 없는 것처럼 nil을 반환합니다. 캐시가 예외를 발생시키는지 파악하려면 예외 수집 서비스에 보고할 error_handler를 제공할 수 있습니다. 이는 세 가지 키워드 인자를 받아야 합니다: method(원래 호출된 캐시 스토어 메서드), returning(사용자에게 반환된 값, 일반적으로 nil), 그리고 exception(처리된 예외).

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

gem "redis"

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

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

좀 더 복잡한 production 환경의 Redis cache store는 다음과 같은 형태일 수 있습니다:

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 }
  }
}

가이드에서 Redis를 캐시 저장소로 사용하려면 ActiveSupport::Cache::RedisCacheStore 사용을 추천합니다. Rails는 mem_cache-client gem을 대체하는 네이티브 Redis 캐시 구현인 redis-activesupport 젬을 권장합니다. memcached를 사용한다면 dalli 젬을 추천합니다. Redis나 memcached 중 하나를 사용하여 프로덕션 환경의 캐싱을 설정할 수 있습니다.

2.7 ActiveSupport::Cache::NullStore

ActiveSupport::Cache::NullStore는 각 웹 요청에 대해 범위가 지정되며, 요청이 끝날 때 저장된 값들을 지웁니다. 개발 및 테스트 환경에서 사용하기 위한 것입니다. Rails.cache와 직접 상호작용하는 코드가 있지만 캐싱이 코드 변경 결과를 확인하는 것을 방해할 때 매우 유용할 수 있습니다.

config.cache_store = :null_store

cache를 비활성화합니다. 이 저장소는 값이 실제로 저장되지 않으므로 테스트에 유용합니다.

2.8 커스텀 Cache Store

단순히 ActiveSupport::Cache::Store를 확장하고 적절한 메서드를 구현하여 자신만의 커스텀 cache store를 만들 수 있습니다. 이를 통해 Rails 애플리케이션에 다양한 캐싱 기술을 적용할 수 있습니다.

커스텀 cache store를 사용하려면, cache store를 여러분의 커스텀 클래스의 새로운 인스턴스로 설정하기만 하면 됩니다.

config.cache_store = MyCacheStore.new

3 Cache Keys

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

캐시 키로 Hash나 Array 값을 사용할 수 있습니다.

# 이것은 올바른 캐시 키입니다
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])

Rails.cache에서 사용하는 키들은 실제 스토리지 엔진에서 사용되는 것과 동일하지 않을 수 있습니다. 네임스페이스가 추가되거나 기술 백엔드의 제약사항에 맞게 변경될 수 있습니다. 예를 들어, Rails.cache로 값을 저장하고 dalli gem을 사용해 직접 꺼내올 수 없다는 것을 의미합니다. 하지만 동시에 memcached 크기 제한을 초과하거나 문법 규칙을 위반하는 것에 대해 걱정할 필요도 없습니다.

4 Conditional GET 지원

Conditional GET은 HTTP 명세의 기능으로, 웹 서버가 브라우저에게 GET 요청에 대한 응답이 마지막 요청 이후 변경되지 않았으며 브라우저 캐시에서 안전하게 가져올 수 있다고 알려주는 방법을 제공합니다.

이는 고유한 콘텐츠 식별자와 콘텐츠가 마지막으로 변경된 타임스탬프를 주고받기 위해 HTTP_IF_NONE_MATCHHTTP_IF_MODIFIED_SINCE 헤더를 사용하여 작동합니다. 브라우저가 요청할 때 콘텐츠 식별자(ETag)나 마지막 수정 타임스탬프가 서버의 버전과 일치하면, 서버는 수정되지 않았다는 상태와 함께 빈 응답만 보내면 됩니다.

서버(즉, 우리)는 마지막 수정 타임스탬프와 if-none-match 헤더를 확인하고 전체 응답을 보낼지 여부를 결정할 책임이 있습니다. Rails의 conditional-get 지원을 사용하면 이는 매우 쉬운 작업이 됩니다:

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

    # 만약 주어진 timestamp와 etag 값에 따라 요청이 낡은 상태(stale)라면
    # (즉, 다시 처리해야 한다면) 이 블록을 실행합니다 
    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
      respond_to do |wants|
        # ... 일반적인 응답 처리
      end
    end

    # 만약 요청이 fresh 상태라면(즉, 수정되지 않았다면) 아무것도 할 필요가 없습니다.
    # 기본 render는 이전 stale? 호출에 사용된 매개변수를 사용하여 이를 확인하고 
    # 자동으로 :not_modified를 전송합니다. 그래서 이게 끝입니다.
  end
end

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

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

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

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

class ProductsController < ApplicationController
  # request가 fresh한 경우 자동으로 :not_modified를 반환하고,
  # stale한 경우 기본 템플릿(product.*)을 렌더링합니다.

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

last_modifiedetag 모두 설정된 경우, config.action_dispatch.strict_freshness 값에 따라 동작이 달라집니다. true로 설정된 경우, RFC 7232 섹션 6에 명시된 대로 etag만 고려됩니다. false로 설정된 경우, 두 조건 모두가 고려되며 두 조건이 모두 만족될 때 캐시가 신선한 것으로 간주됩니다. 이는 기존 Rails의 동작 방식이었습니다.

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

기본적으로 캐시된 응답은 private으로 설정되어 사용자의 웹 브라우저에만 캐시됩니다. 프록시가 응답을 캐시하도록 허용하려면, 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

4.1 Strong vs Weak ETags

Rails는 기본적으로 weak ETag를 생성합니다. Weak ETag는 응답 본문이 정확히 일치하지 않더라도 의미적으로 동일한 응답에 대해 동일한 ETag를 가질 수 있게 합니다. 이는 응답 본문의 사소한 변경에 대해 페이지를 재생성하고 싶지 않을 때 유용합니다.

Weak ETag는 strong ETag와 구분하기 위해 앞에 W/가 붙습니다.

W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag (약한 ETag)
"618bbc92e2d35ea1945008b42799b0e7" → Strong ETag (강한 ETag)

weak ETag와 달리, strong ETag는 응답이 정확히 동일하고 바이트 단위로 일치해야 함을 의미합니다. 큰 용량의 비디오나 PDF 파일에서 Range 요청을 할 때 유용합니다. Akamai와 같은 일부 CDN은 strong ETag만 지원합니다. strong ETag를 반드시 생성해야 하는 경우 다음과 같이 할 수 있습니다.

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
    # 마지막 수정 시간과 strong ETag를 사용하여 HTTP 캐싱 헤더를 설정합니다.
  end
end

응답에 strong ETag를 직접 설정할 수도 있습니다.

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

5 Caching in Development

기본적으로 개발 모드에서는 :memory_store를 통한 caching이 활성화되어 있습니다. 이는 기본적으로 비활성화되어 있는 Action Controller caching에는 적용되지 않습니다.

Action Controller caching을 활성화하려면 Rails에서 제공하는 bin/rails dev:cache 명령어를 사용하세요.

$ bin/rails dev:cache
Development 모드가 현재 캐싱되고 있습니다.
$ bin/rails dev:cache
Development 모드가 더 이상 캐싱되지 않습니다.

캐싱을 비활성화하려면 cache_store:null_store로 설정하세요

6 References



맨 위로