Discourse Rails 애플리케이션을 작업할 때, 플러그인을 구축하거나 discourse/discourse에 풀 리퀘스트를 보내는 경우 N+1 쿼리 문제를 마주치는 여러 가지 상황이 있습니다. 이 주제는 이러한 문제를 해결하고 Discourse의 성능을 유지하기 위해 데이터 사전 로딩(preloading)을 처리하는 방법을 설명합니다.
Rails에서의 N+1 쿼리 문제
먼저, N+1 쿼리 문제와 Rails가 이를 해결하는 방식에 익숙하지 않다면 가이드의 해당 섹션을 확인해 보세요.
토픽(Topics)
개별 토픽을 로드할 때 주의해야 할 N+1 문제는 게시글(post)과 연관된 테이블을 로드할 때 발생합니다. 이제, 개별 토픽에 직렬화된 데이터는 TopicView 클래스에서 준비되고 사전 로딩됩니다. 이 클래스에는 주요 쿼리가 실행되기 전에 연관된 테이블을 사전 로딩할 수 있는 on_preload 훅이 있습니다. ActivityPub 플러그인에서 TopicView.on_preload 사용의 좋은 예시를 볼 수 있습니다.
이 훅의 사용은 아래에서 다룰 커스텀 필드의 사전 로딩과 함께 토픽 내 게시글의 추가 연관 항목을 로드하는 것입니다. 여기서 ActiveRecord::Associations::Preloader를 사용하고 있음을 알 수 있습니다. 이 클래스에 대한 자세한 내용은 문서를 참고할 수 있습니다:
토픽 목록(Topic Lists)
토픽 목록을 로드할 때 주의해야 할 N+1 문제는 토픽과 연관된 테이블을 로드할 때 발생합니다. 개별 토픽을 위한 TopicView 클래스와 유사하게, 토픽 목록을 위한 TopicList 클래스가 있습니다. TopicView와 마찬가지로 TopicList에도 쿼리가 데이터베이스에 도달하기 전에 연관된 테이블을 로드하기 위해 메인 토픽 목록 쿼리에 연결할 수 있는 on_preload 메서드가 있습니다. Discourse Assign 플러그인에서 이에 대한 좋은 예시를 볼 수 있습니다:
register_topic_preloader_associations
TopicList 클래스에는 ActivityPub 플러그인에서 보았던 것과 동일한 패턴을 적용하는 Server Plugin API 메서드인 register_topic_preloader_associations가 있습니다. 이는 ``ActiveRecord::Associations::Preloader`를 사용합니다. 연관 항목의 배열을 전달하면 이 작업을 대신 수행해 줍니다.
register_topic_list_preload_user_ids
모든 필요한 토픽을 가져오기 위해 하나의 큰 쿼리를 실행하는 것 외에도, TopicList 클래스는 목록에 필요한 모든 사용자를 가져오기 위해 또 다른 큰 쿼리를 실행합니다. 여기에는 각 항목이 포함됩니다:
- 토픽 사용자(topic user)
- 마지막 게시글 작성자(last post user)
- 토픽 추천 사용자(topic featured user)
- 토픽 허용 사용자(topic allowed users)
각 토픽에 대해 다른 사용자를 로드하고 있다면, 이 API 메서드를 사용하여 해당 사용자의 ID를 큰 사용자 쿼리에 포함시킬 수 있습니다. 그러면 나머지 데이터와 함께 사전 로딩됩니다.
카테고리(Categories)
사이트의 여러 곳에서 사용되는 카테고리 메인 목록은 Site 모델에서 로드되고 캐시됩니다. 엄밀한 의미의 사전 로딩은 아니지만, 유사한 작업을 수행하므로 여기 포함해 보았습니다.
site_all_categories_cache_query 수정자(modifier)
all_categories_cache 메서드에는 큰 카테고리 쿼리를 수정할 수 있는 Server Plugin API 수정자가 있습니다: site_all_categories_cache_query. 이 수정자는 plugin.rb에서 다음과 같이 사용합니다:
register_modifier(:site_all_categories_cache_query) do |query|
query.where("categories.name LIKE 'Cool%'")
end
검색(Search)
검색 사전 로딩도 코드에서 그렇게 명시적으로 설명되지는 않지만, 쿼리를 통해 유사한 작업, 즉 이저 로딩(eager loading)을 수행합니다.
register_search_topic_eager_load
Server Plugin API 메서드 register_search_topic_eager_load를 사용하면 검색에서 추가 테이블을 이저 로딩할 수 있습니다. 예를 들어:
register_search_topic_eager_load do |opts|
%i(example_table)
end
커스텀 필드(Custom Fields)
커스텀 필드는 HasCustomFields 컨서른(concern)에 의해 구동됩니다. 이 컨서른은 연관된 커스텀 필드를 가진 모델, 즉 Post, Topic, Category, Group에 포함됩니다. HasCustomFields 모듈에는 다양한 방식으로 사용할 수 있는 자체 사전 로딩 시스템이 있습니다. HasCustomFields 사전 로딩을 사용하는 가장 좋은 방법은 Server Plugin API 메서드를 사용하는 것이며, 지금까지 설명한 내용을 고려하면 비교적 자명할 것입니다.
register_preloaded_category_custom_fields
add_preloaded_group_custom_field
add_preloaded_topic_list_custom_field
필요할 때 ActivityPub 플러그인 예시에서 보았듯이, HasCustomFields의 일부 낮은 수준 메서드를 사용하여 직접 사전 로딩을 수행할 수도 있습니다. 그 예시에서 HasCustomFields 컨서른의 preload_custom_fields는 TopicView.on_preload에서 게시글 커스텀 필드를 사전 로딩하는 데 사용됩니다.
TopicView.on_preload do |topic_view|
##
Post.preload_custom_fields(topic_view.posts, activity_pub_post_custom_field_names)
##
end