본문 바로가기
Ruby on Rails

Named scope로 db 조회를 최적화 해보자

by 혜리루 2019. 10. 31.

 

Ruby on Rails를 공부하면서 낯설었던 개념중에 하나는 scope였습니다. scope라고 하면 변수가 살아있는(!) 범위를 뜻하는 scope가 바로 떠올라서 그런지 Ruby on Rails의 scope 키워드가 정확히 어떤 기능을 하는지 잘 와닿지 않았습니다. 그러던 중 scope에 대해 쉽게 잘 설명해놓은

How to optimize your search with Named Scopes in Rails 5 라는 영문블로그 글을 찾아서 개념도 정리할겸 번역을 해보았습니다..

#오역주의

 

 

* 아래 글에서 얘기하는 필터는 rails의 컨트롤러의 액션 앞뒤에서 실행하는 filter가 아니라 where문을 통해 데이터 베이스안에 데이터를 필터링하여 조회하는 메소드나 코드를 말함.

 

* 아래 글에서 수차례 scope 키워드가 active record association을 리턴한다고 언급하는데, 실제로 ActiveRecord::Relation오브젝트를 리턴함. ActiveRecord::Associations은 foreign key를 통해 오브젝트끼리 묶어주는 class method임.

 

 

* named scope : Adds a class method for retrieving and querying objects. The method is intended to return an ActiveRecord::Relation object, which is composable with other scopes. If it returns nil or false, an all scope is returned instead.

 

출처:https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html

 

 


내가 2010년에 루비온레일즈를 처음 시작하던 당시에 나는 당최 named scope라는 놈이 뭔지 이해할 수가 없었다.  그때 나는 혼자서  메소드를 작성하는 것이나 Active record 쿼리를 쓰는 게 능숙하지 않았다 (솔직히 데이터베이스 마이그레이션이 뭔지도 몰랐다.)

scope는 그냥 마치 레일즈의 마법의 자동완성 부품중 하나 같았고 그래서 나는 scope를 가능한한 계속 무시하고 안썼다.

 

model은 뚱뚱하게 controller는 날씬하게. 이건 내가 지난 몇년동안 루비온레일즈를 사용하면서 배운 법칙들중 하나다. 

쉽게 말하면 controller는 간단해야 한다는 것이다. controller는 view로 정보를 전달하고, 데이터베이스에 레코드를 저장, 삭제, 수정하는 것을 'control'한다. model은 컨트롤러로 제공할 정보를 정의하고, 정제하고, 필터링한다. 어플레이케이션이 커지게 되면 컨트롤러 안의 간단한 필터(simple filter)들은 재사용가능한 조회 쿼리나 모델의 필터로 대체하는 것이 좋은데 여기서 바로 named scope가 쓰인다.

named scope가 뭐야?

named scope는 그냥 active record association을 리턴하는 모델 클래스 메소드다. 

 

이런 어플리케이션이 있다고 생각해보자

 

우리는 shirt에 대한 데이터베이스가 있고 우리는 모든 빨간색 shirt들을 리턴하고 싶다. 우리는 아래의 방법들을 따라할 수 있을 것이다.

 

1. scope와 class method 둘다 빨간색 셔츠를 리턴하는 아주 간단한 필터를 만들수 있다. 컨트롤러 안에서 우리는 아래의 메소드들 중에 하나를 호출할 수 있다.  

#app/models/shirt.rb

scope :red, -> {where(color: "red")}

def self.red
 where(color: "red")
end

이놈들은 모두 모든 빨간색 셔츠를 리턴한다. 하지만 좀 기초적이다. 이런식이라면 색깔마다 다른 메소드가 필요하게 된다.

 

#app/controllers/shirts_controller.rb

def index
  @shirts = Shirt.all.red  #그냥 Shirt.red라고 해도 된다.
end

 

2. 가능한 모든 색깔들을 고려해서 메소드를 추상화시키면 조금 나아진다.

 

#app/models/shirt.rb

scope :colored, -> (color) {where(color: color)}

def self.colored(color)
  where(color: color)
end

scope와 클래스 method 모두 색깔에 대한 인자를 받아서 해당 색깔의 셔츠를 리턴해준다. 

 

3. 위의 방식의 쿼리는 SQL injection 공격을 당하기 쉽다. 아래의 문법을 사용해서 query를 escape하면 더 안전하다. 

 

#app/models/shirt.rb

scope :colored, -> (color) {where("color LIKE ?", color)

def colored(color)
  where("color LIKE ?", color)
end

 

 

컨트톨러 안에서는 아래처럼 쿼리를 써서 조회를 할 수 있다.

 

#app/controllers/shirts_controller.rb

def index
  @color = params[:color] #이건 form에서 가져오는 데이터다.
  @shirts = Shirt.colored(@color)
end

 

 

이 모든 예제들 안에서 named scope와 class method는 모두 같은 데이터를 리턴한다. 하지만 named scope를 이용하면 단 한줄만에 간단하게 작성을 할 수 있다는 장점이 있었다.

그렇다면 언제 class method 대신 named scope 를 사용할까??

 

scope를 자주 사용하는 경우중에 하나는 'soft delete' 사용할 때다. user에 대한 데이터베이스가 있는 웹사이트를 생각해보자. user가 웹사이트를 탈퇴했을 때도 대부분의 경우 실제 데이터가 지워지지는 않는다. 데이터를 보관하려는 이유일 수도 있고, 사용자가 같은 이메일로 재가입하는 경우를 방지하기 위해서 이메일 정보를 저장해둘 수도 있다. (예를들면 신규가입자 무료체험을 계속 쓰지 못하도록) 'soft delete'에서는 user가 탈퇴했을때 실제로 데이터가 지워지는게 아니라 '비활성화'된다. 즉 어떤 user를 찾으려고 쿼리를 날릴때 db상에 존재하는 모든 user를 하나씩 비교하면서 찾는게 아니라 현재 '활성화된' user만 비교해보면 된다. (아니면 유료회원만 대상으로 찾는다든지 기타 등등)

scope를 이용하면 아래처럼 진짜 쉽게 활성화된 user를 찾을 수 있다.

   

#app/models/user.rb

scope :active, -> {where(status: "active")}
scope :inactive, -> {where(status: "inactive")}

#app/controllers/users_controller.rbdef index 
 @users = User.all.active 
end

이게 뭐 어쨌는데?

어플리케이션이 커지다보면 다양한 user에 대해서 다양한 필터가 필요하게 될 것이다. (특정 도시에 거주하는 user나 특정한 역할이 있는 user 등). 샌프란시스코나 뉴욕에서만 서비스하다가 미국의 다른 도시들, 그리고 전세계로까지 범위를 넓힌 우버나 에어비앤비 같은 앱들을 생각해보면 좋겠다.

named scope는 언제나 nil이 아닌 active record association을 리턴해준다. 따라서 필터 하나가 빈 결과를 조회하더라도 조회를 실패하지 않기 때문에 named scope를 만들고싶은만큼 안전하게 만들 수 있다. nil이 발생할 수 있는 상황으로부터 언제나 보호된다는 말이다. 

 

scope는 class method와 어떻게 다를까?

 

named scope 는 두가지 큰 장점이 있다. Chaining(줄지어 연결시키기)과 가독성.

Chaining

scope는 object relation을 리턴한다. 이 말은 쿼리의 결과가 없더라도 nil이 아닌 빈 배열을 리턴한다는 뜻이다. 따라서 2개 이상의 scope들을 함께 연결시키더라도 nil이 나오지않을까 걱정하지 않아도 된다. 바로 이렇게 :  Article.published.featured.latest_article

가독성

scope는 단 한줄만에 작성할 수 있다. scope는 애플리케이션이 커지더라도 model을dry(?)하게 유지할 수 있는 좋은 방법이다. 여러 개의 if문 대신에 삼항연산자를 사용하는 때를 생각해보자. 이런걸 보면 고인물 개발자와 뉴비 개발자를 구분할 수 있다.

scope :recent, -> { order(:created_at, :desc) }

 

언제 named scope를 쓰지 말아야할까?

scope를 쓰지 말아야하는 가장 큰 두가지 상황은 1)쿼리가 너무 복잡할때나 2)db 조회가 애플리케이션의 속도를 저하시킬때다.

복잡성

위에서 얘기한 것처럼 named scope는 active record나 SQL을 이용해서 필터를 만드는 완벽한 방법이다. scope를 쓰면 where, order, limit 등등의 문을 단 한 줄만에 쉽게 쓸 수 있다. 하지만 약간 더 복잡한 필터라면 class method를 쓰는게 더 나은 방법일 수 도 있다. 하나의  긴 method가 여러개의 scope보다 오히려 읽기 쉬울 수도 있다.

속도

시간에 지남에 따라 규모가 커지는 애플리케이션이라면 성능은 아주 중요한 이슈다. scope를 호출하는 매순간마다 우리는 db에 쿼리를 날리게 된다. 이러한 쿼리의 반복(iteration)은 n+1 쿼리를 발생시킨다.(애플리케이션 속도를 아주 느리게 한다) scope를 사용하면 쿼리를 실행하는 대신 db에 쿼리를 날리기 때문에 active record association으로 데이터를 pre-load하는게 나을 수도 있다. (여기 무슨 뜻인지 모르겠음..) Justin Weiss라는 사람이 속도 저하를 막기위해서 scope를 preload하는 방법에 대한 환상적인 글을하나 작성했다.

 

https://www.justinweiss.com/articles/how-to-preload-rails-scopes//?source=post_page-----5a0444d8b759----------------------

 

How to preload Rails scopes

 

www.justinweiss.com

 

핵심정리

 

한줄요약을 보기위해서 글을 대충 넘긴 사람을 위해서 두가지 주요 포인트들을 남겨주겠다.

 

1. Controller안에 간단한 데이터베이스 조회 필터를 넣지 마라.

2. 유연하고 읽기 쉽고 안전한 조회 필터를 만들려면 Model안에서 named scope를 써라

 

 

출처: How to optimize your search with Named Scopes in Rails 5

https://medium.com/le-wagon/what-are-named-scopes-and-how-to-use-them-rails-5-5a0444d8b759

댓글