데이터베이스를 죽이지 마세요: Rails 앱을 더 빠르게 만드는 5가지 ActiveRecord 팁
Source: Dev.to
이 글에서는 ActiveRecord와 함께한 나의 여정을 공유합니다. 처음 Rails를 시작했을 때는 ActiveRecord가 데이터베이스에 데이터를 저장해 주는 “마법”이라고만 생각했었습니다. 하지만 규모가 큰 프로젝트를 진행하면서 훨씬 더 많은 것이 있다는 것을 깨달았습니다. 동작하는 코드를 쉽게 작성할 수 있지만, 앱을 느리게 만드는 코드를 쉽게 작성할 수도 있습니다. 아래는 간단한 쿼리부터 고급 최적화까지 다섯 단계의 ActiveRecord 지식 수준을 정리한 것입니다.
LEVEL 1: 기본 (체이닝)
대부분의 초보자는 find와 where를 알고 있습니다. ActiveRecord의 멋진 점은 데이터를 실제로 요청할 때까지 이 메서드들을 무한히 체이닝할 수 있다는 것입니다.
# Instead of doing this
users = User.where(active: true)
recent_users = users.where('created_at > ?', 1.day.ago)
# You can chain it nicely
User.where(active: true).where('created_at > ?', 1.day.ago)
이 코드는 하나의 SQL 쿼리만 생성합니다. Rails는 데이터베이스에 접근하는 시점을 가능한 마지막 순간까지 미룹니다.
LEVEL 2: 스코프로 정리하기
여러 컨트롤러에서 동일한 where 조건을 반복하는 것은 번거롭습니다. 모델에 스코프를 정의하면 클래스 메서드처럼 사용할 수 있습니다.
# app/models/post.rb
class Post { where(published: true, deleted: false) }
scope :recent, -> { where('created_at > ?', 1.week.ago) }
end
이제 컨트롤러 코드는 훨씬 깔끔해집니다:
# app/controllers/posts_controller.rb
def index
@posts = Post.live.recent
end
LEVEL 3: N+1 문제 해결 (고급)
가장 흔한 실수는 부모 레코드를 로드한 뒤 자식 레코드를 반복하면서 각 연관 관계마다 별도의 쿼리를 발생시키는 것입니다.
# controller
@posts = Post.all
# view (ERB)
Rails는 포스트에 대해 1개의 쿼리를 실행하고, 사용자에 대해 N개의 추가 쿼리를 실행합니다(포스트당 1개). 100개의 포스트라면 총 101개의 쿼리가 실행되어 성능이 크게 저하됩니다.
STEP 1: includes 사용
연관 레코드를 한 번의 쿼리로 가져오도록 ActiveRecord에 알려줍니다.
@posts = Post.includes(:user).all
Rails는 이제 2개의 쿼리만 실행합니다:
- 모든 포스트를 가져옵니다.
- 해당 포스트와 연관된 모든 사용자를 가져옵니다.
LEVEL 4: Pluck vs Map (최적화)
단일 컬럼만 필요할 때는 전체 ActiveRecord 객체를 로드하지 않는 것이 좋습니다.
# Heavy: loads all User objects into memory
User.all.map(&:email)
대신 pluck을 사용하면 데이터베이스에서 바로 컬럼을 가져올 수 있습니다:
# Fast: fetches only the email column
User.pluck(:email)
pluck은 모델 객체를 인스턴스화하지 않고 문자열 배열을 반환하므로 메모리와 시간을 절약합니다.
LEVEL 5: present? 대신 exists? 사용
레코드가 존재하는지만 확인하고 싶다면 레코드를 로드하지 마세요.
# Bad: loads the post into memory just to check existence
if Post.where(id: params[:id]).present?
# …
end
# Good: runs a lightweight SQL `SELECT 1` with LIMIT 1
if Post.exists?(params[:id])
# …
end
exists?는 불리언 값을 반환하고, 매치가 발견되는 즉시 쿼리를 중단합니다.
이 몇 가지 트릭을 마스터하면 Rails 애플리케이션이 더 빠르게 동작하고 코드도 더 깔끔해집니다.