Rails 앱 성능 향상: N+1 쿼리 최적화

Speed Up Your Rails App by Squashing N+1 Queries

작성자
발행일
2025년 11월 20일

핵심 요약

  • 1 N+1 쿼리는 Rails 앱의 흔한 성능 저하 원인이지만, `includes` 메서드를 통해 단 한 줄의 코드로 쉽게 해결할 수 있습니다.
  • 2 `includes`를 사용하여 관련된 ActiveRecord 모델을 미리 로드함으로써, 수많은 개별 쿼리를 단 몇 개의 대량 쿼리로 통합하여 데이터베이스 부하를 줄일 수 있습니다.
  • 3 실제 사례에서 1,101개의 데이터베이스 쿼리를 단 3개로 줄이고 페이지 로드 시간을 87% 개선하여 현저한 성능 향상을 달성했습니다.

도입

N+1 쿼리는 Rails 애플리케이션에서 가장 흔한 성능 저하의 원인 중 하나로, 관련된 ActiveRecord 모델을 반복하면서 연결된 레코드의 필드에 접근할 때 발생합니다. 이는 데이터베이스에 불필요하게 많은 쿼리를 발생시켜 애플리케이션의 속도를 현저히 늦출 수 있습니다. 본 포스팅에서는 이러한 N+1 쿼리 문제를 진단하고, Rails의 강력한 기능인 `includes` 메서드를 활용하여 어떻게 효율적으로 해결할 수 있는지 구체적인 사례를 통해 설명합니다.

N+1 쿼리는 ActiveRecord 모델 간의 연관 관계에서 발생합니다. 예를 들어, Author가 여러 Post를 가지고, 각 Post가 여러 Tag를 가지는 상황을 가정해봅시다. 저자 목록을 표시하면서 각 저자의 게시물과 태그를 함께 보여주려 할 때, 연관된 데이터를 로드하는 방식에 따라 N+1 쿼리가 발생할 수 있습니다.

N+1 쿼리 문제 이해

Author, Post, Tag 모델이 다음과 같이 정의되어 있다고 가정합니다: ruby class Author < ApplicationRecord has_many :posts end class Post < ApplicationRecord belongs_to :author has_many :tags end class Tag < ApplicationRecord belongs_to :post end 이러한 모델 관계에서 저자의 게시물과 태그를 반복적으로 접근할 때, Rails는 기본적으로 각 연관 레코드에 대해 별도의 쿼리를 실행하게 됩니다. 이는 데이터가 많아질수록 쿼리 수가 기하급수적으로 증가하는 N+1 문제를 야기합니다.

최적화 전: 비효율적인 쿼리

최적화되지 않은 상태에서 저자, 게시물, 태그를 모두 가져오는 코드는 다음과 같습니다. ```ruby # AuthorsController

index

@authors = Author.all.order(:name) # app/views/authors/index.html.erb <% @authors.each do |author| %>

<%= author.name %>

    <% author.posts.each do |post| %>
  • <%= post.title %> - <%= post.tags.map(&:name).join(", ") %>
  • <% end %>

<% end %> ``` 이 경우, 3명의 저자가 각각 3개의 게시물을 가지고 각 게시물이 3개의 태그를 가진다고 할 때, 총 13개의 쿼리가 발생합니다. 만약 100명의 저자, 각 10개의 게시물, 각 5개의 태그가 있다면 총 1,101개의 쿼리가 발생하며, 페이지 로드 시간이 1.007초까지 지연될 수 있습니다.

부분 최적화: includes(:posts)

Rails는 N+1 쿼리 문제를 해결하기 위해 includes 메서드를 제공합니다. 먼저 게시물만 미리 로드하여 부분적으로 최적화할 수 있습니다. ruby class AuthorsController < ApplicationController def index @authors = Author.all.includes(:posts).order(:name) end end 이 변경으로 저자와 게시물은 단일 쿼리로 미리 로드되지만, 태그에 대한 N+1 쿼리는 여전히 남아있습니다. 100명의 저자 예시에서 총 1,002개의 쿼리로 줄어들고, 페이지 로드 시간은 1.007초에서 0.678초로 32% 개선됩니다.

완전 최적화: includes(posts: [:tags])

includes 메서드는 해시를 사용하여 중첩된 연관 관계도 미리 로드할 수 있습니다. 게시물뿐만 아니라 태그까지 함께 미리 로드하여 완벽하게 최적화할 수 있습니다. ruby class AuthorsController < ApplicationController def index @authors = Author.all.includes(posts: [:tags]).order(:name) end end 이 코드를 사용하면 저자, 게시물, 태그 모두 단일 쿼리로 대량 선택되어 총 3개의 쿼리만 발생합니다. 페이지 로드 시간은 0.678초에서 0.126초로 81% 추가 개선되어, 초기 상태 대비 87%의 성능 향상을 이룰 수 있습니다.

결론

이처럼 Rails의 `includes` 메서드를 활용하면 단 한 줄의 코드 변경만으로도 N+1 쿼리 문제를 효과적으로 해결하고 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 개별적으로는 작은 개선처럼 보일 수 있지만, 여러 페이지에 걸쳐 발생하는 N+1 쿼리들을 최적화하면 사용자 경험에 직접적인 영향을 미치는 전반적인 앱 속도 저하를 방지할 수 있습니다. 느린 성능을 당연시하기보다는, 적극적인 최적화를 통해 빠르고 효율적인 애플리케이션을 구축하는 것이 중요합니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!