Rails에서 .any?를 잘못 사용하는 것을 멈추세요

발행: (2026년 2월 25일 오전 12:30 GMT+9)
7 분 소요
원문: Dev.to

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

  1. 데이터베이스에서 모든 포스트를 로드합니다.
  2. 각 행에 대해 ActiveRecord 객체를 인스턴스화합니다.
  3. Ruby에서 순회합니다.
  4. 불리언 값을 반환합니다.

개발 환경에서는 별다른 문제가 없어 보일 수 있지만, 프로덕션에서는 재앙이 될 수 있습니다. 사용자가 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 코드를 작성할 수 있고 숨겨진 성능 함정을 방지할 수 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »

Rails에서 타임스탬프 전환 간소화

개요 나는 종종 completed_at와 같은 timestamps를 boolean 플래그로 사용한다. 실제 boolean보다 약간 더 많은 메타 데이터를 제공하지만 UI에서는 일반적으로 …

솔로 프론트엔드 팀: Pure Ruby로 UI 시스템 구축

'Partial' 문제 우리는 Rails를 사랑합니다. ERB도 사랑합니다. 하지만 솔직히 말해서, app/views는 보통 어떤 Rails 코드베이스에서도 가장 지저분한 부분입니다. 처음엔 간단하게 시작합니다. 그런데…