캐싱은 나중에 빠르게 검색할 수 있도록 일부 코드의 결과를 저장하는 것을 의미하는 일반적인 용어입니다. 이를 통해 예를 들어 거의 변경되지 않는 데이터를 얻기 위해 데이터베이스에 계속 부딪치는 것을 피할 수 있습니다. 일반적인 개념은 모든 유형의 캐싱에 대해 동일하지만 Rails는 캐싱하려는 항목에 따라 다양한 지원을 제공합니다.
Rails 개발자의 경우 일반적인 캐싱 형식에는 메모이제이션, 저수준 캐싱(둘 다 이 캐싱 시리즈의 이전 부분에서 다룸) 및 뷰 캐싱이 포함되며 여기에서 다룰 것입니다.
Ruby on Rails가 뷰를 렌더링하는 방법
먼저 약간 혼란스러운 용어를 살펴보겠습니다. Rails 커뮤니티에서 "views"라고 부르는 것은 app/views
내부에 있는 파일입니다. 예배 규칙서. 일반적으로 .html.erb
입니다. 다른 옵션이 있지만(예:일반 .html
, .js.erb
또는 Slim 및 haml과 같은 다른 전처리기를 사용하는 파일). 다른 많은 웹 프레임워크에서 이러한 파일을 "템플릿"이라고 하며, 이 파일이 용도를 더 잘 설명한다고 생각합니다.
Rails 애플리케이션이 GET
를 수신할 때 요청이 있는 경우 특정 컨트롤러 작업(예:UsersController#index
)으로 라우팅됩니다. . 그런 다음 작업은 데이터베이스에서 필요한 정보를 수집하고 보기/템플릿 파일을 렌더링하는 데 사용하기 위해 전달합니다. 이 시점에서 우리는 "뷰 레이어"로 들어가고 있습니다.
일반적으로 보기(또는 템플릿)는 하드 코딩된 HTML 마크업과 동적 Ruby 코드가 혼합되어 있습니다.
#app/views/users/index.html.erb
<div class='user-list'>
<% @users.each do |user| %>
<div class='user-name'><%= user.name %></div>
<% end %>
</div>
보기를 렌더링하려면 파일의 루비 코드를 실행해야 합니다(erb
<% %>
의 모든 것입니다. 태그). 페이지를 100번 새로고침하고 @users.each...
100번 실행됩니다. 포함된 모든 부분에 대해서도 마찬가지입니다. 프로세서는 부분 html.erb
를 로드해야 합니다. 파일에서 모든 Ruby 코드를 실행하고 결과를 단일 HTML 파일로 결합하여 요청자에게 다시 보냅니다.
저속 보기의 원인
개발 중에 페이지를 볼 때 Rails가 다음과 같은 많은 로그 정보를 출력한다는 사실을 눈치채셨을 것입니다.
Processing by PagesController#home as HTML
Rendering layouts/application.html.erb
Rendering pages/home.html.erb within layouts/application
Rendered pages/home.html.erb within layouts/application (Duration: 4.0ms | Allocations: 1169)
Rendered layouts/application.html.erb (Duration: 35.9ms | Allocations: 8587)
Completed 200 OK in 68ms (Views: 40.0ms | ActiveRecord: 15.7ms | Allocations: 14307)
마지막 줄은 이 단계에서 우리에게 가장 유용합니다. 왼쪽에서 오른쪽으로 시간을 따라가면 Rails가 브라우저에 응답을 반환하는 데 걸린 총 시간이 68ms였으며 그 중 erb
를 렌더링하는 데 40ms가 소요되었음을 알 수 있습니다. 파일 및 ActiveRecord 쿼리 처리 시 15.7ms.
사소한 예이지만 뷰 레이어 캐싱을 살펴봐야 하는 이유도 보여줍니다. 마술처럼 ActiveRecord 쿼리가 즉시 발생하도록 할 수 있다고 해도 erb
를 렌더링하는 데 두 배 이상 시간이 소요됩니다. .
뷰 렌더링이 느릴 수 있는 몇 가지 이유가 있습니다. 예를 들어 뷰 내에서 값비싼 DB 쿼리를 호출하거나 루프 내에서 많은 작업을 수행할 수 있습니다. 내가 본 가장 일반적인 상황 중 하나는 단순히 여러 수준의 중첩을 사용하여 많은 부분을 렌더링하는 것입니다.
개별 행을 처리하는 부분이 있을 수 있는 이메일 받은 편지함을 상상해 보십시오.
# app/views/emails/_email.html.erb
<li class="email-line">
<div class="email-sender">
<%= email.from_address %>
</div>
<div class="email-subject">
<%= email.subject %>
</div>
</div>
그리고 기본 받은 편지함 페이지에서 각 이메일에 대한 부분을 렌더링합니다.
# app/views/emails/index.html.erb
...
<% @emails.each do |email| %>
<%= render email %>
<% end %>
받은 편지함에 100개의 메시지가 있는 경우 _email.html.erb
를 렌더링합니다. 부분 100번. 우리의 사소한 예에서 이것은 크게 우려할 사항이 아닙니다. 내 컴퓨터에서 부분은 전체 인덱스를 렌더링하는 데 15ms만 걸립니다. 물론 실제 예제는 더 복잡하고 그 안에 다른 부분을 포함할 수도 있습니다. 렌더링 시간을 늘리는 것은 어렵지 않습니다. _email
을 렌더링하는 데 1-2ms밖에 걸리지 않더라도 부분적인 경우 전체 수집을 수행하는 데 100-200ms가 걸립니다.
다행스럽게도 Rails에는 __email
만 캐시할 것인지 여부와 상관없이 이 문제를 해결하기 위해 캐싱을 쉽게 추가하는 데 도움이 되는 몇 가지 내장 기능이 있습니다. 부분, index
페이지 또는 둘 다.
보기 캐싱이란 무엇입니까
Ruby on Rails의 뷰 캐싱은 뷰가 생성하는 HTML을 가져와 나중에 사용할 수 있도록 저장합니다. Rails는 이것을 파일 시스템에 쓰거나 메모리에 유지하는 기능을 지원하지만 프로덕션 용도로 사용하려면 Memcached 또는 Redis와 같은 독립 실행형 캐싱 서버가 필요할 것입니다. Rails의 memory_store
개발에는 유용하지만 프로세스 간에 공유할 수 없습니다(예:여러 서버/dyno 또는 유니콘과 같은 분기 서버). 마찬가지로 file_store
서버에 로컬입니다. 따라서 여러 상자에서 공유할 수 없으며 만료된 항목을 자동으로 삭제하지 않으므로 Rails.cache.clear
를 주기적으로 호출해야 합니다. 서버의 디스크가 꽉 차는 것을 방지합니다.
캐싱 저장소를 활성화하려면 환경 구성 파일(예:config/environments/production.rb
):
# memory store is handy for testing
# during development but not advisable
# for production
config.cache_store = :memory_store
기본 설치에서 development.rb
컴퓨터에서 캐싱을 쉽게 토글할 수 있도록 일부 구성이 이미 완료되어 있습니다. rails dev:cache
를 실행하기만 하면 됩니다. 캐싱을 켜고 끕니다.
Rails에서 뷰를 캐싱하는 것은 믿을 수 없을 정도로 간단하므로 성능 차이를 설명하기 위해 sleep(5)
를 사용하겠습니다. 인위적 지연 생성:
<% cache do %>
<div>
<p>Hi <%= @user.name %>
<% sleep(5) %>
</div>
<% end %>
이 뷰를 처음 렌더링하는 데 예상대로 5초가 걸립니다. 그러나 두 번째 로드는 cache do
내부의 모든 것이 수행되기 때문에 몇 밀리초 밖에 걸리지 않습니다. 캐시에서 블록을 가져옵니다.
예시별 보기 캐싱 추가
작은 예제 보기를 사용하여 캐싱 옵션을 살펴보겠습니다. 이 보기가 실제로 몇 가지 성능 문제를 일으키는 것으로 가정합니다.
# app/views/user/show.html.erb
<div>
Hi <%= @user.name %>!
<div>
<div>
Here's your list of posts,
you've written
<%= @user.posts.count %> so far
<% @user.posts.each do |post|
<div><%= post.body %></div>
<% end %>
</div>
<% sleep(5) #artificial delay %>
이것은 인공적인 5초 지연과 함께 작업할 기본 골격을 제공합니다. 먼저 전체 show.html.erb
를 래핑할 수 있습니다. cache do
의 파일 앞에서 설명한 대로 차단합니다. 이제 캐시가 따뜻해지면 멋지고 빠른 렌더링 시간을 얻을 수 있습니다. 하지만 이 계획에 문제가 발생하는 데는 오랜 시간이 걸리지 않습니다.
첫째, 사용자가 이름을 변경하면 어떻게 됩니까? 캐시된 페이지를 언제 만료할지 Rails에 알려주지 않았으므로 사용자는 업데이트된 버전을 볼 수 없습니다. 쉬운 해결책은 @user
를 전달하는 것입니다. cache
에 대한 개체 방법:
<% cache(@user) do %>
<div>
Hi <%= @user.name %>!
</div>
...
<% sleep(5) #artificial delay %>
<% end %>
저수준 캐싱에 대한 이 시리즈의 이전 기사에서는 캐시 키에 대한 세부 정보를 다루었으므로 여기서 다시 다루지 않겠습니다. 지금은 모델을 cache()
에 전달하면 충분합니다. , 해당 모델의 updated_at
를 사용합니다. 캐시에서 조회할 키를 생성하는 속성입니다. 즉, @user
이 캐시된 페이지는 만료되고 Rails는 HTML을 다시 렌더링합니다.
사용자가 이름을 변경하는 경우를 처리했지만 게시물은 어떻습니까? 기존 게시물을 변경하거나 새 게시물을 생성해도 updated_at
는 변경되지 않습니다. User
의 타임스탬프 , 따라서 캐시된 페이지가 만료되지 않습니다. 또한 사용자가 이름을 변경하면 모두 게시물이 변경되지 않았더라도 이 두 가지 문제를 모두 해결하려면 "러시아 인형 캐싱"(즉, 캐시 내의 캐시)을 사용할 수 있습니다.
<% cache(@user) do %>
<div>
Hi <%= @user.name %>!
<div>
<div>
Here's your list of posts,
you've written
<%= @user.posts.count %> so far<br>
<% @user.posts.each do |post| %>
<% cache(post) do %>
<div><%= post.body %></div>
<% end %>
<% end %>
</div>
<% sleep(5) #artificial delay %>
<% end %>
이제 개별적으로 렌더링된 각각의 post
를 캐싱합니다. (실제 세계에서 이것은 아마도 부분적일 것입니다). 따라서 @user
업데이트되면 게시물을 다시 렌더링할 필요가 없습니다. 캐시된 값만 사용할 수 있습니다. 하지만 아직 한 가지 문제가 더 있습니다. post
변경되었지만 @user.update_at
때문에 여전히 업데이트를 렌더링하지 않습니다. 변경되지 않았으므로 cache(@user) do
실행되지 않습니다.
이 문제를 해결하려면 touch: true
를 추가해야 합니다. Post
로 모델:
class Post < ApplicationRecord
belongs_to :user, touch: true
end
touch: true
추가 여기서 우리는 ActiveRecord에 매번 게시물이 업데이트되었습니다. updated_at
를 원합니다. 업데이트할 "소속" 사용자의 타임스탬프입니다.
또한 Rails는 얼마나 일반적인지 고려할 때 부분 컬렉션을 렌더링하기 위한 특정 도우미를 제공한다고 덧붙였습니다.
<%= render partial: 'posts/post',
collection: @posts, cached: true %>
다음과 기능적으로 동일합니다.
<% @posts.each do |post| %>
<% cache(post) do %>
<%= render post %>
<% end %>
<% end %>
render partial: ... cached: true
일 뿐만 아니라 덜 장황한 형태로, Rails가 컬렉션의 각 항목에 대한 캐시 저장소를 누르는 대신 캐시 저장소에 다중 가져오기를 발행할 수 있기 때문에(즉, 단일 왕복으로 많은 키/값 쌍을 읽을 수 있기 때문에) 추가 효율성을 제공합니다.
동적 페이지 콘텐츠
일부 페이지에는 주변 페이지의 나머지 부분보다 훨씬 빠른 속도로 변경되는 '동적' 콘텐츠가 포함되어 있는 것이 일반적입니다. 이는 활동/뉴스 피드가 있을 수 있는 홈 페이지 또는 대시보드에서 특히 그렇습니다. 캐시된 페이지에 이러한 항목을 포함하면 캐시를 자주 무효화해야 하므로 애초에 캐싱을 통해 얻을 수 있는 이점이 제한될 수 있습니다.
간단한 예로 현재 날짜를 보기에 추가해 보겠습니다.
<% cache(@user) do %>
<div>
Hi <%= @user.name %>,
hope you're having a great
<%= Date.today.strftime("%A") %>!
<div>
...
<% end %>
우리는 매일 캐시를 무효화할 수 있지만 명백한 이유 때문에 그다지 실용적이지 않습니다. 한 가지 옵션은 자리 표시자 값(또는 빈 <span>
) 자바스크립트로 채웁니다. 이러한 종류의 접근 방식은 종종 "자바스크립트 스프링클"이라고 하며 많은 Rails의 핵심 코드가 개발되는 Basecamp에서 선호하는 접근 방식입니다. 결과는 다음과 같을 것입니다:
<% cache(@user) do %>
<div>
Hi <%= @user.name %>,
hope you're having a great
<span id='greeting-day-name'>Day</span>!
<div>
...
<% end %>
<script>
// assuming you're using vanilla JS with turbolinks
document.addEventListener(
"turbolinks:load", function() {
weekdays = new Array('Sunday', 'Monday',
'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday');
today = weekdays[new Date().getDay()];
document.getElementById("greeting-day-name").textContent=today;
});
</script>
또 다른 접근 방식은 보기의 일부만 캐시하는 것입니다. 이 예에서 인사말은 페이지 상단에 있으므로 다음 항목만 캐시하는 것은 매우 간단합니다.
<div>
Hi <%= @user.name %>,
hope you're having a great
<%= Date.today.strftime("%A") %>!
<div>
<% cache(@user) do %>
...
<% end %>
분명히 이것은 실제 세계에서 볼 수 있는 레이아웃으로 간단하지 않은 경우가 많으므로 캐싱을 적용하는 위치와 방법에 대해 신중해야 합니다.
경고 문구
뷰 캐싱을 성능 문제에 대한 빠르고 쉬운 만병통치약으로 보는 것은 쉽습니다. 실제로 Rails는 이를 믿을 수 없을 정도로 만듭니다. 뷰와 부분이 깊게 중첩된 경우에도 캐시하기 쉽습니다. 이 시리즈의 첫 번째 기사에서 시스템에 캐싱을 추가할 때 발생할 수 있는 문제를 설명했지만, 특히 보기 수준 캐싱의 경우에 그렇습니다.
그 이유는 뷰가 본질적으로 시스템의 기본 데이터와 더 많은 상호 작용을 하는 경향이 있기 때문입니다. Rails에서 메모이제이션이나 저수준 캐싱을 적용할 때 캐시된 값을 새로 고쳐야 하는 시기와 이유를 결정하기 위해 파일 외부를 볼 필요가 없는 경우가 많습니다. 반면에 보기에는 여러 다른 모델이 호출될 수 있으며 의도적인 계획 없이는 어느 모델이 보기의 어느 부분을 어느 시점에 다시 렌더링해야 하는지 확인하기 어려울 수 있습니다.
저수준 캐싱과 마찬가지로 가장 좋은 조언은 언제 어디서 사용하는지에 대해 전략적으로 정하는 것입니다. 허용 가능한 수준의 성능을 달성하려면 가능한 한 적은 위치에서 가능한 한 적은 캐싱을 사용하십시오.
Rails의 기본 캐싱
지금까지 이 캐싱 시리즈에서 수동으로 캐싱하는 방법을 다루었지만 수동 구성 없이도 ActiveRecord는 이미 쿼리 속도를 높이기 위해(또는 완전히 건너뛰기 위해) 내부에서 일부 캐싱을 수행합니다. 캐싱에 관한 이 시리즈의 다음 기사에서는 ActiveRecord가 우리를 위해 캐싱하는 것이 무엇인지, 그리고 적은 양의 작업으로 thing.children.size
최신 카운트를 얻기 위해 데이터베이스를 조회할 필요가 전혀 없습니다.