⚠️

N+1 쿼리 문제

성능의 적 — includes로 해결하는 방법

N+1 문제는 Rails에서 가장 흔한 성능 문제입니다.

문제 상황:

# Controller
@posts = Post.all  # 쿼리 1: SELECT * FROM posts

# View
<% @posts.each do |post| %>
  <%= post.user.name %>  # 쿼리 N: SELECT * FROM users WHERE id = ? (매번 실행!)
<% end %>

포스트가 100개면 101번 쿼리 실행!

해결: includes

@posts = Post.includes(:user)  # 쿼리 2번으로 해결
# SELECT * FROM posts
# SELECT * FROM users WHERE id IN (1, 2, 3, ...)

3가지 방법:

  • includes — Rails가 자동으로 최적 전략 선택

  • preload — 별도 쿼리로 로드 (기본)

  • eager_load — LEFT OUTER JOIN 사용

탐지 도구: bullet gem이 N+1을 자동 감지하여 경고를 표시합니다.

구조 다이어그램

N+1 문제 (101 쿼리!)
1. SELECT * FROM posts
2. SELECT * FROM users WHERE id = 1
3. SELECT * FROM users WHERE id = 2
4. SELECT * FROM users WHERE id = 3
... (N번 반복!)
101. SELECT * FROM users WHERE id = 100
Post.all → post.user (매번 쿼리)
includes 적용 (2 쿼리!)
1. SELECT * FROM posts
2. SELECT * FROM users WHERE id IN (1, 2, 3, ..., 100)
끝! 단 2번의 쿼리로 완료
Post.includes(:user) → 미리 로드
코드 비교:
Before
@posts = Post.all
After
@posts = Post.includes(:user)
핵심: <strong>includes 한 줄</strong>로 101번 쿼리를 2번으로 줄임 — bullet gem으로 자동 탐지 가능

핵심 포인트

1

문제 인식: 반복문 안에서 연관 데이터에 접근하면 N+1 발생

2

rails 로그에서 반복되는 SELECT 쿼리 확인

3

includes(:association)를 컨트롤러 쿼리에 추가

4

중첩 관계: includes(posts: :comments) 또는 includes(:posts, :profile)

5

bullet gem 설치로 N+1 자동 감지

6

strict_loading 모드로 N+1 발생 시 예외 발생하도록 설정 (Rails 6.1+)

장점

  • includes 한 줄로 성능 수십 배 향상
  • bullet gem으로 자동 탐지 가능
  • strict_loading으로 런타임 방지 가능
  • SQL 로그로 쉽게 확인 가능

단점

  • 모든 곳에 includes를 넣으면 불필요한 데이터 로딩
  • eager_load는 JOIN으로 인한 데이터 중복 가능
  • 복잡한 관계에서는 최적 전략 판단 필요
  • 메모리 사용량 증가 가능

사용 사례

게시글 목록 + 작성자 표시 주문 목록 + 상품 정보 카테고리 + 하위 아이템 대시보드 통계 페이지