Rails에서 .any?를 잘못 사용하는 것을 멈추세요
Source: Dev.to
소개
.any?에 전달된 단일 블록은 경고도 오류도 없이 수천 개의 레코드를 메모리로 조용히 로드할 수 있습니다—불필요한 객체만 생깁니다. 대부분의 Rails 개발자는 이를 눈치채지 못합니다.
.any?와 .exists?는 모두 같은 간단한 질문에 답합니다: 레코드가 하나라도 존재합니까?
하지만 내부적으로는 두 메서드가 매우 다르게 동작할 수 있습니다. 이 글에서는 각 메서드를 호출했을 때 무슨 일이 일어나는지, 언제 어떤 메서드를 사용해야 하는지, 그리고 흔히 발생하는 성능 함정을 어떻게 피할 수 있는지 설명합니다.
기본 사용법 – 존재 여부만 확인
관계에 레코드가 하나라도 있는지 여부만 확인하면 될 경우, .any?와 .exists?는 동일한 효율적인 쿼리를 생성합니다:
user.posts.any?
#=> SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1
user.posts.exists?
#=> SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1
객체가 메모리로 로드되지 않습니다. 두 메서드 모두 데이터베이스에 간단한 예/아니오 질문을 하고 첫 번째 매치를 찾은 즉시 반환합니다.
.where를 체인할 때도 마찬가지이며, 블록을 .any?에 전달하지 않는 한 동일하게 동작합니다:
user.posts.where(published: true).any?
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = true
# LIMIT 1
user.posts.where(published: true).exists?
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = true
# LIMIT 1
이것만 필요하다면 코드 가독성이 더 좋은 쪽을 선택하면 됩니다; 성능 차이는 없습니다.
.any? With a Block
블록을 .any?에 전달하는 순간, Rails는 동작을 완전히 바꿉니다. 데이터베이스에 질의하는 대신 모든 일치하는 레코드를 메모리로 로드하고 Ruby에서 필터링합니다:
user.posts.any? { |post| post.published? }
#=> SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
What Happens
- 데이터베이스에서 모든 포스트를 로드합니다.
- 각 행에 대해
ActiveRecord객체를 인스턴스화합니다. - Ruby에서 순회합니다.
- 불리언 값을 반환합니다.
개발 환경에서는 별다른 문제가 없어 보일 수 있지만, 프로덕션에서는 재앙이 될 수 있습니다. 사용자가 50,000개의 포스트를 가지고 있다면, 하나라도 공개된 포스트가 있는지 확인하기 위해 50,000개의 객체를 메모리에 로드한 셈입니다.
Implementation Insight
def any?(*args)
return false if @none
return super if args.present? || block_given?
!empty?
end
블록이 주어지면 Rails는 Enumerable#any?에 위임하게 되며, 이는 전체 컬렉션을 메모리에 보관해야 합니다. 즉, 필터링을 SQL에서 Ruby로 옮긴 것입니다.
Prefer SQL for Filtering
조건을 데이터베이스에 직접 전달하세요:
user.posts.exists?(published: true)
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = TRUE
- 한 행, 한 컬럼.
- 첫 번째 매치를 찾으면 즉시 중단.
- 객체 인스턴스화 없음.
같은 결과를 훨씬 낮은 비용으로 얻을 수 있습니다.
Source:
.any? 가 실제로 올바른 선택일 때
중요한 예외가 있습니다: 관계가 이미 로드된 경우, .any? 는 데이터베이스를 다시 조회하지 않습니다.
users = User.includes(:posts)
users.each do |user|
user.posts.any? { |post| post.published? } # 추가 쿼리 없음
end
posts 가 미리 로드되었기 때문에, .any? 는 전적으로 메모리 내에서 동작합니다. 이 상황에서는:
.any?→ 추가 쿼리 없음..exists?→ 새로운 SQL 쿼리를 강제 실행.
여기서 .exists? 를 사용하면 불필요한 데이터베이스 호출이 발생하고, 심지어 N+1 패턴을 만들 수도 있습니다. Rails 는 관계가 로드되었는지를 내부적으로 확인하고, .any? 는 일반 Ruby 컬렉션처럼 동작합니다.
가이드라인 요약
| 관계 상태 | 권장 메서드 |
|---|---|
| 로드되지 않음 | exists? (또는 블록 없이 any?) |
| 이미 로드됨 | 블록 없이 any? |
| 로드되지 않은 관계에 블록을 적용해야 할 경우 | 블록이 있는 .any? 를 피하고, 조건을 포함한 SQL (exists? with conditions) 사용 |
결론
ActiveRecord API의 작은 차이가 실제 운영에 영향을 줄 수 있습니다. .any?를 호출하기 전에 스스로에게 물어보세요:
- 단순히 존재 여부만 확인하고 있나요? →
exists?(또는 블록 없이any?)를 사용하세요. - 전체 컬렉션을 로드하려고 하나요? → 로드되지 않은 관계에 블록을 사용한
.any?를 피하세요.
이러한 뉘앙스를 이해하면 더 효율적인 Rails 코드를 작성할 수 있고 숨겨진 성능 함정을 방지할 수 있습니다.