클래스 메소드와 Scope. 선택은?

Justin Weiss의 Should You Use Scopes or Class Methods?를 번역한 글입니다.


Scope은 데이터베이스에서 원하는 객체를 찾아오는 훌륭한 방법이다.

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

다음처럼 사용한다.

@recent_reviews = Review.most_recent(5)

그렇지만 scope을 호출하는 방법은 Review 모델의 클래스 메소드와 완전히 같다. 그리고 클래스 메소드도 그다지 어렵지도 않다.

def self.most_recent(limit)
  order("created_at desc").limit(limit)
end
@recent_reviews = Review.most_recent(5)

그렇다면 왜 루비의 클래스 메소드 대신에 scope을 사용할까? 같은 개념을 다른 방법으로 분리해서 이해하는게 의미가 있을까? 이상한 버그를 만나게 되지 않을까? 레일즈를 배우기 어렵게 만드는 군더더기 아닐까?

언제 클래스 메소드 대신에 scope을 사용하면 좋을까?

이미 클래스 메소드가 있는데 왜 scope을 사용할까?

특정 날짜 이후에 작성한 리뷰를 모으고 싶은데, 대신 날짜를 명시하지 않으면 모든 리뷰를 반환한다고 가정하자.

scope으로는 다음과 같다.

scope :created_since, ->(time) { where("reviews.created_at > ?", time) if time.present? }

쉽다. 클래스 메소드의 경우는 어떨까?

def self.created_since(time)
  if time.present?
    where("reviews.created_at > ?", time)
  else
    all
  end
end

약간 더 수고를 해야 한다. scope은 대부분 scope을 반환하기에 체인으로 연결해서 사용하기 편하다.

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

그렇지만 클래스 메소드를 같은 방법으로 사용하려면, 날짜가 nil인 경우를 처리해야 하거나 호출하는 코드에서 유효성 여부를 검증해야 한다.

항상 같은 종류의 객체를 반환하는 메소드는 매우 유용하다. 예외 상황이나 오류에 대해 별로 걱정을 할 필요가 없다. 항상 이용할 수 있는 값을 반환받는다고 가정할 수 있다.

그 말은 nil을 신경쓰지 않고 scope을 연결할 수 있다는 말이다.

항상 scope을 반환한다는 가정에 예외 상황이 있기는 하다.

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

하지만 아직까지 현업에서 이런 코드를 본 적은 없다.

scope에서 제일 좋아하는 요소는 의도를 보여줄 수 있다는 점이다. 코드를 읽고 있는 사람에게 이렇게 말할 수 있다. “이 메소드는 연결할 수 있고, 객체 목록을 반환하여 원하는 객체 집합을 선택할 수 있게 해준다.” 이는 일반적인 클래스 메소드 의미보다 훨씬 많은 정보이다.

언제 scope 대신 클래스 메소드를 사용해야 하나?

scope으로는 의도를 표현할 수 있기 때문에, 가능한 wherelimit 같은 간단한 내장 scope을 연결하여, 보다 복잡한 scope을 만들어 이용한다.

여기에 두 가지 예외가 있다.

  1. scope을 사전 적재 해야 하는 경우, 대신 association을 이용해야 한다.
  2. 내장 scope을 연결하는 수준을 넘어서는 작업을 하는 경우, 클래스 메소드를 이용한다.

scope 로직이 복잡해지면, 클래스 메소드가 더 적합할 수 있다.

클래스 메소드로는 루비 코드와 데이터베이스 코드를 자유롭게 섞을 수 있다. 예를 들어 정렬 코드를 루비로 작성하는게 편하다면, 객체를 기본 순서로 내려 받은 후에 sort_by를 이용하여 원하는 순서로 나열할 수 있다.

아니면 데이터베이스, 레디스, 외부 API, 서비스 같은 각기 다른 저장소에서 데이터를 수집하기가 번거롭다면, 클래스 메소드에서 데이터를 내려 받은 후 하나의 객체 모음으로 조합하여, 마치 배열을 반환하는 scope 처럼 사용할 수 있다.

이 경우에도 scope로 질의, 정렬, 조인, 필터링을 구현할 수 있다. 그런 다음 클래스 메소드 구현시에 scope를 활용하면 클래스 메소드와 scope로 더 깔끔하게 애플리케이션을 구현할 수 있다.


scope은 내가 선호하는 레일즈 기능 중 하나이다. scope의 강력한 기능을 확인하고 싶다면 내가 쓴 다른 글 레일즈 모델 정렬하고 필터링하기에서 유용한 scope 예제를 볼 수 있다.

그리고 scope을 마스터하는 간단한 방법이 있다. 작은 앱으로 이것저것 해보는 것이다. Practicing Rails의 무료 샘플로 시도해 보자!