필드 수준 Rails 캐시 무효화: DSL 솔루션
게시 됨: 2022-03-11현대 웹 개발에서 캐싱은 속도를 높이는 빠르고 강력한 방법입니다. 캐싱을 제대로 수행하면 애플리케이션의 전체 성능이 크게 향상될 수 있습니다. 잘못하면 반드시 재앙으로 끝날 것입니다.
아시다시피 캐시 무효화는 컴퓨터 과학에서 가장 어려운 세 가지 문제 중 하나입니다. 다른 두 가지는 이름 지정과 개별 오류입니다. 한 가지 쉬운 방법은 무언가가 변경될 때마다 왼쪽과 오른쪽을 모두 무효화하는 것입니다. 그러나 그것은 캐싱의 목적을 무효화합니다. 절대적으로 필요한 경우에만 캐시를 무효화하려고 합니다.
캐싱을 최대한 활용하려면 무효화하는 항목에 대해 매우 세심한 주의를 기울여야 하며 반복 작업에 귀중한 리소스를 낭비하지 않도록 애플리케이션을 저장해야 합니다.
이 블로그 게시물에서는 Rails 캐시가 작동하는 방식, 특히 필드 수준 캐시 무효화를 구현하는 방법을 더 잘 제어하는 기술을 배우게 됩니다. 이 기술은 Rails ActiveRecord 및 ActiveSupport::Concern
과 touch
방식 동작 조작에 의존합니다.
이 블로그 게시물은 필드 수준 캐시 무효화를 구현한 후 성능이 크게 개선된 프로젝트의 최근 경험을 기반으로 합니다. 불필요한 캐시 무효화와 템플릿의 반복적인 렌더링을 줄이는 데 도움이 되었습니다.
Rails, Ruby 및 성능
Ruby는 가장 빠른 언어는 아니지만 전반적으로 개발 속도와 관련하여 적합한 옵션입니다. 또한 메타프로그래밍 및 기본 제공 DSL(도메인 특정 언어) 기능은 개발자에게 엄청난 유연성을 제공합니다.
Jakob Nielsen의 연구와 같은 연구에 따르면 작업이 10초 이상 걸리면 초점을 잃는다는 것을 보여줍니다. 그리고 초심을 되찾는 데는 시간이 걸립니다. 따라서 이것은 예기치 않게 비용이 많이 들 수 있습니다.
불행히도 Ruby on Rails에서는 템플릿 생성으로 10초 임계값을 초과하기가 매우 쉽습니다. "hello world" 앱이나 소규모 애완 동물 프로젝트에서는 그런 일이 발생하지 않지만 단일 페이지에 많은 것이 로드되는 실제 프로젝트에서는 템플릿 생성이 매우 쉽게 끌리기 시작할 수 있습니다.
그리고 그것이 바로 제가 제 프로젝트에서 해결해야 했던 것입니다.
간단한 최적화
하지만 정확히 어떻게 속도를 높이나요?
답: 벤치마킹하고 최적화하십시오.
내 프로젝트에서 최적화의 두 가지 매우 효과적인 단계는 다음과 같습니다.
- N+1 쿼리 제거
- 템플릿을 위한 좋은 캐싱 기술 소개
N+1 쿼리
N+1 쿼리를 수정하는 것은 쉽습니다. 할 수 있는 일은 로그 파일을 확인하는 것입니다. 로그에 아래와 같은 여러 SQL 쿼리가 표시될 때마다 즉시 로드로 교체하여 제거합니다.
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
이 비효율성을 감지하는 데 도움이 되는 총알이라고 하는 보석이 있습니다. 또한 각 사용 사례를 살펴보고 위의 패턴과 대조하여 로그를 검사하여 로그를 확인할 수도 있습니다. N+1의 비효율성을 모두 제거함으로써 데이터베이스에 과부하가 걸리지 않고 ActiveRecord에 소요되는 시간이 크게 줄어들 것이라는 확신을 가질 수 있습니다.
이러한 변경을 수행한 후 내 프로젝트는 이미 더 활발하게 실행되고 있었습니다. 하지만 다음 단계로 넘어가서 로드 시간을 더 줄일 수 있는지 알아보기로 했습니다. 템플릿에서 발생하는 불필요한 렌더링이 상당히 많았고 궁극적으로 프래그먼트 캐싱이 도움이 되었습니다.
프래그먼트 캐싱
조각 캐싱은 일반적으로 템플릿 생성 시간을 크게 줄이는 데 도움이 됩니다. 그러나 기본 Rails 캐시 동작은 내 프로젝트에서 잘리지 않았습니다.
Rails 프래그먼트 캐싱의 이면에 있는 아이디어는 훌륭합니다. 매우 간단하고 효과적인 캐싱 메커니즘을 제공합니다.
Ruby On Rails의 저자는 Signal v. Noise에서 조각 캐싱이 작동하는 방식에 대한 아주 좋은 기사를 작성했습니다.
엔터티의 일부 필드를 표시하는 약간의 사용자 인터페이스가 있다고 가정해 보겠습니다.
- 페이지 로드 시 Rails는 엔티티의 클래스와
updated_at
필드를 기반으로cache_key
를 계산합니다. - 해당
cache_key
를 사용하여 해당 키와 연결된 캐시에 항목이 있는지 확인합니다. - 캐시에 아무 것도 없으면 해당 조각의 HTML 코드가 뷰에 대해 렌더링되고 새로 렌더링된 콘텐츠는 캐시에 저장됩니다.
- 캐시에 해당 키가 있는 기존 콘텐츠가 있으면 캐시의 콘텐츠로 보기가 렌더링됩니다.
이는 캐시를 명시적으로 무효화할 필요가 없음을 의미합니다. 엔터티를 변경하고 페이지를 다시 로드할 때마다 엔터티에 대해 새 캐시 콘텐츠가 렌더링됩니다.
Rails는 기본적으로 자식이 변경되는 경우 부모 엔터티의 캐시를 무효화하는 기능도 제공합니다.
belongs_to :parent_entity, touch: true
이것은 모델에 포함될 때 자식이 터치 될 때 자동으로 부모를 터치 합니다. 여기에서 touch
에 대해 자세히 알아볼 수 있습니다. 이를 통해 Rails는 부모 엔터티의 캐시를 자식 엔터티의 캐시와 동시에 무효화하는 간단하고 효율적인 방법을 제공합니다.
Rails의 캐싱
그러나 Rails의 캐싱은 부모 엔터티를 나타내는 HTML 조각이 부모의 자식 엔터티만을 나타내는 HTML 조각을 포함하는 사용자 인터페이스를 제공하기 위해 만들어졌습니다. 즉, 이 패러다임에서 자식 엔터티를 나타내는 HTML 조각은 부모 엔터티의 필드를 포함할 수 없습니다.
그러나 그것은 현실 세계에서 일어나는 일이 아닙니다. Rails 애플리케이션에서 이 조건을 위반하는 작업을 수행해야 할 수도 있습니다.
사용자 인터페이스가 자식 엔터티를 나타내는 HTML 조각 내부에 부모 엔터티의 필드를 표시하는 상황을 어떻게 처리할까요?
자식에 부모 엔터티의 필드가 포함되어 있으면 Rails의 기본 캐시 무효화 동작에 문제가 있습니다.
상위 엔티티에서 제공된 필드가 수정될 때마다 해당 상위에 속한 모든 하위 엔티티를 터치해야 합니다. 예를 들어, Parent1
이 수정되면 Child1
및 Child2
보기에 대한 캐시가 모두 무효화되었는지 확인해야 합니다.
분명히 이것은 엄청난 성능 병목 현상을 일으킬 수 있습니다. 부모가 변경될 때마다 모든 자식 엔터티를 건드리면 정당한 이유 없이 많은 데이터베이스 쿼리가 발생합니다.
또 다른 유사한 시나리오는 has_and_belongs_to
연결과 연결된 엔터티가 목록에 표시되고 해당 엔터티를 수정하면 연결 체인을 통해 캐스케이드 캐시 무효화가 시작된 경우입니다.

class Event < ActiveRecord::Base has_many :participants has_many :users, through: :participants end class Participant < ActiveRecord::Base belongs_to :event belongs_to :user end class User < ActiveRecord::Base has_many :participants has_many :events, through :participants end
따라서 위의 사용자 인터페이스의 경우 사용자의 위치가 변경될 때 참가자 또는 이벤트를 터치하는 것은 비논리적입니다. 하지만 사용자 이름이 변경되면 이벤트와 참가자 모두를 터치해야 하지 않겠습니까?
따라서 Signal v. Noise 기사의 기술은 위에서 설명한 대로 특정 UI/UX 인스턴스에 대해 비효율적입니다.
Rails는 간단한 작업에 매우 효과적이지만 실제 프로젝트에는 고유한 복잡성이 있습니다.
필드 레벨 레일스 캐시 무효화
내 프로젝트에서 위와 같은 상황을 처리하기 위해 작은 Ruby DSL을 사용했습니다. 이를 통해 연결을 통해 캐시 무효화를 트리거할 필드를 선언적으로 지정할 수 있습니다.
실제로 도움이 되는 몇 가지 예를 살펴보겠습니다.
예 1:
class Event < ActiveRecord::Base include Touchable ... has_many :tasks ... touch :tasks, in_case_of_modified_fields: [:name] ... end class Task < ActiveRecord::Base belongs_to :event end
이 스니펫은 Ruby의 메타프로그래밍 기능과 내부 DSL 기능을 활용합니다.
더 구체적으로 말하면 이벤트의 이름 변경만 관련 작업의 프래그먼트 캐시를 무효화합니다. 목적이나 위치와 같은 이벤트의 다른 필드를 변경해도 작업의 조각 캐시가 무효화되지 않습니다. 나는 이 필드 수준의 세분화된 캐시 무효화 제어 라고 부를 것입니다.
예 2:
has_many
연관 체인을 통한 캐시 무효화를 보여주는 예를 살펴보겠습니다.
아래 표시된 사용자 인터페이스 조각은 작업과 해당 소유자를 보여줍니다.
이 사용자 인터페이스의 경우 작업을 나타내는 HTML 조각은 작업이 변경되거나 소유자 이름이 변경된 경우에만 무효화되어야 합니다. 소유자의 다른 모든 필드(예: 시간대 또는 기본 설정)가 변경되면 작업 캐시는 그대로 유지되어야 합니다.
이것은 여기에 표시된 DSL을 사용하여 달성됩니다.
class User < ActiveRecord::Base include Touchable touch :tasks, in_case_of_modified_fields: [:first_name, :last_name] ... end class Task < ActiveRecord::Base has_one owner, class_name: :User end
DSL 구현
DSL의 핵심은 touch
방식입니다. 첫 번째 인수는 연결이고 다음 인수는 해당 연결에 대한 touch
를 트리거하는 필드 목록입니다.
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
이 메서드는 Touchable
모듈에서 제공합니다.
module Touchable extend ActiveSupport::Concern included do before_save :check_touchable_entities after_save :touch_marked_entities end module ClassMethods def touch association, options @touchable_associations ||= {} @touchable_associations[association] = options end end end
이 코드에서 요점은 touch
호출의 인수를 저장한다는 것입니다. 그런 다음 엔터티를 저장하기 전에 지정된 필드가 수정된 경우 연결을 더티로 표시합니다. 연결이 더러우면 저장 후 해당 연결의 엔터티를 만집니다.
그런 다음 우려의 사적인 부분은 다음과 같습니다.
... private def klass_level_meta_info self.class.instance_variable_get('@touchable_associations') end def meta_info @meta_info ||= {} end def check_touchable_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_pair do |association, change_triggering_fields| if any_of_the_declared_field_changed?(change_triggering_fields) meta_info[association] = true end end end def any_of_the_declared_field_changed?(options) (options[:in_case_of_modified_fields] & changes.keys.map{|x|x.to_sym}).present? end …
check_touchable_entities
메소드 에서 선언된 필드가 변경 되었는지 확인합니다. 그렇다면 meta_info[association]
를 true
로 설정하여 연결을 더티로 표시합니다.
그런 다음 엔터티를 저장한 후 더티 연결 을 확인하고 필요한 경우 엔터티를 터치합니다.
… def touch_marked_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_key do |association_key| if meta_info[association_key] association = send(association_key) association.update_all(updated_at: Time.zone.now) meta_info[association_key] = false end end end …
그리고, 바로 그것입니다! 이제 간단한 DSL을 사용하여 Rails에서 필드 수준 캐시 무효화를 수행할 수 있습니다.
결론
Rails 캐싱은 상대적으로 쉽게 애플리케이션의 성능 향상을 약속합니다. 그러나 실제 응용 프로그램은 복잡할 수 있으며 종종 고유한 문제를 제기할 수 있습니다. 기본 Rails 캐시 동작은 대부분의 시나리오에서 잘 작동하지만 캐시 무효화에서 조금 더 최적화하면 먼 길을 갈 수 있는 특정 시나리오가 있습니다.
이제 Rails에서 필드 수준 캐시 무효화를 구현하는 방법을 알았으므로 애플리케이션에서 불필요한 캐시 무효화를 방지할 수 있습니다.