Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Rails 스코프(Scope)와 클래스 메서드, 무엇을 사용해야 할까?

스코프(Scope)는 데이터베이스에서 원하는 객체를 손쉽게 가져오는 훌륭한 도구입니다.

app/models/review.rb
class Review < ActiveRecord::Base
  scope :most_recent, -> (limit) { order("created_at desc").limit(limit) }
end

스코프는 다음과 같이 호출해서 사용합니다.

app/controllers/homepage_controller.rb
@recent_reviews = Review.most_recent(5)

그런데 잘 보면 이 스코프를 호출하는 모습이 Review의 클래스 메서드를 호출하는 것과 완전히 똑같습니다. 게다가 같은 기능을 클래스 메서드로도 아주 쉽게 만들 수 있습니다.

app/models/review.rb
def self.most_recent(limit)
  order("created_at desc").limit(limit)
end
app/controllers/homepage_controller.rb
@recent_reviews = Review.most_recent(5)

일반적인 Ruby 클래스 메서드로도 충분한데, 굳이 스코프를 써야 할 이유가 있을까요? 기능은 같지만 완전히 별개인 두 개념을 머릿속에 따로 담아둘 가치가 있을까요? 혹시 알 수 없는 버그를 만나게 되면 어떻게 될까요? 이런 부가적인 개념들이 오히려 Rails 학습을 더 어렵게 만드는 원인은 아닐까요?

그렇다면 언제 클래스 메서드 대신 스코프를 사용하는 것이 합리적일까요?

이미 클래스 메서드가 있는데, 왜 스코프를 사용해야 할까?

특정 날짜 이후에 작성된 리뷰만 가져오고 싶다고 가정해 봅시다. 단, 날짜가 지정되지 않았다면 모든 리뷰를 반환하고 싶다면 어떻게 해야 할까요?

스코프로 구현하면 이렇습니다.

app/models/review.rb
scope :created_since, ->(time) { where("reviews.created_at > ?", time) if time.present? }

아주 간단하죠? 그럼 클래스 메서드로는 어떨까요?

app/models/review.rb
def self.created_since(time)
  if time.present?
    where("reviews.created_at > ?", time)
  else
    all
  end
end

클래스 메서드는 약간 더 신경 써야 할 부분이 생깁니다. 스코프는 기본적으로 스코프를 반환하도록 설계되어 있어 체인으로 연결하기 매우 쉽습니다.

Review.positive.created_since(5.days.ago)

반면 클래스 메서드로 같은 동작을 만들려면, time이 nil인 경우를 직접 처리해 주어야 합니다. 그렇지 않으면 반환값이 체인 가능한 유효한 스코프인지 호출자(caller)가 매번 판단해야 하거든요.

항상 동일한 종류의 객체를 반환하는 메서드는 정말 유용합니다. 엣지 케이스나 오류 상황을 크게 걱정할 필요가 없고, 언제나 바로 사용할 수 있는 객체가 돌아올 것이라고 믿을 수 있기 때문입니다.

여기서의 의미는, nil이 반환될까 두려워하지 않고도 자유롭게 스코프를 체인으로 연결할 수 있다는 점입니다.

물론 "항상 스코프가 반환된다"는 가정을 깨뜨릴 수 있는 코드도 작성할 수는 있습니다.

app/models/review.rb
scope :broken, -> { "Hello!!!" }
irb(main):001:0> Review.broken.most_recent(5)
NoMethodError: undefined method `most_recent' for "Hello!!!":String

다만 실무 코드에서 이런 일이 실제로 발생한 경험은 없습니다.

제가 스코프에서 가장 좋아하는 점은 바로 '의도(intent)'를 표현한다는 사실입니다. 코드를 읽는 다음 사람에게 "이 메서드는 체인으로 연결할 수 있고, 최종적으로는 객체 목록이 되며, 원하는 객체 집합을 골라내는 데 도움을 준다"라고 말해주는 셈입니다. 이것은 범용적인 클래스 메서드가 전달하는 정보보다 훨씬 많은 것입니다.

그렇다면 언제 클래스 메서드를 사용해야 할까?

스코프는 의도를 명확히 표현하기 때문에, where, limit 같은 간단한 내장 스코프들을 조합해 더 복잡한 스코프를 만들 때마다 저는 스코프를 사용합니다. 애초에 스코프는 '원하는 객체 집합 찾기'를 위해 설계된 기능이니까요.

하지만 두 가지 예외가 있습니다.

  1. 스코프를 미리 로드(preload)해야 하는 경우에는 액소시에이션(association)으로 전환합니다.
  2. 내장 스코프를 체인하는 것 이상의 복잡한 작업이 필요한 경우에는 클래스 메서드를 사용합니다.

스코프의 로직이 복잡해지기 시작하면, 클래스 메서드가 더 자연스러운 위치처럼 느껴집니다.

클래스 메서드 안에서는 Ruby 코드와 데이터베이스 코드를 자유롭게 섞을 수 있습니다. 예를 들어 Ruby로 작성하는 편이 훨씬 쉬운 정렬 로직이 있다면, 객체를 기본 순서대로 가져온 뒤 sort_by로 원하는 순서를 만들면 됩니다.

조금 더 나아가면, 하나의 클래스 메서드가 데이터베이스, Redis, 외부 API 등 여러 곳에서 데이터를 가져와 조합할 수도 있습니다. 그리고 그 결과를 마치 배열로 변환된 스코프처럼 느껴지는 객체 컬렉션으로 포장할 수도 있습니다.

그런 경우에도 선택, 정렬, 조인, 필터링 관련 코드는 스코프 안에 두고, 클래스 메서드 안에서 그 스코프들을 활용하는 것이 좋습니다. 그러면 더 깔끔하고 읽기 쉬운 클래스 메서드를 얻을 수 있고, 애플리케이션 전반에서 재사용할 수 있는 스코프도 확보하게 됩니다.

마치며

스코프는 제가 가장 사랑하는 Rails 기능 중 하나입니다. 활용법만 익히면 정말 강력한 결과를 만들어낼 수 있습니다. Rails 모델의 정렬과 필터링에 관한 글에서 특히 유용한 스코프 예제를 확인해 보시길 권합니다.

스코프를 완벽하게 익히는 가장 간단한 방법은, 작고 집중된 앱 안에서 직접 실험해 보는 것입니다. Practicing Rails의 무료 샘플 챕터에서 그 구체적인 방법을 확인할 수 있습니다. 지금 바로 살펴보세요!