본문 바로가기
Ruby on Rails

named scope안에서 last 메소드를 사용했을 때의 문제 해결

by 혜리루 2019. 11. 28.

얼마전 ruby on rails 개발을 하던중 이상한 점을 하나 발견했습니다.

빈 값을 반환해야할 named scope가 그 반대인 전체 레코드를 모두 반환하는 것이 그것이었습니다.

 

제가 의도했던 코드의 로직은 아주 간단했습니다.

 

1. 데이터를 필터링함

2. 제일 마지막 record를 가져옴

 

그래서 저는 아무 생각 없이 model에 named scope를 만들어 주었습니다.

class Model
    scope :example, -> {
    where(name: 'haeree').last
  }
end

그리고 실행 결과는 이러했습니다.

Model.where(name: 'haeree')
# => [] 
Model.example
# => [모델1, 모델2, 모델3]

 

또잉???

 

저는 첫번째 라인처럼 where함수가 반환하는 relation의 결과가 비어있다면 example scope는 nil을 반환하거나 exception을 던질것이라고 예상했습니다.

하지만 결과는 정 반대였습니다. 해당되는 model의 모든 active record가 반환되고 있었습니다.

 

저는 처음에 제가 어딘가에서 실수를 했거나 active record 클래스의 버그가 있는줄 알고 열심히 삽질을 했습니다.

그러다가 active record의 소스코드를 찾아보게 되었는데요, 아주 명쾌한 답을 얻을 수 있었습니다.

 

# active_record/scoping/named.rb


# 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}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead.
        
        ... 중략
        
        def scope(name, body, &block)
          unless body.respond_to?(:call)
            raise ArgumentError, "The scope body needs to be callable."
          end

          ... 중략
          
          if body.respond_to?(:to_proc)
            singleton_class.define_method(name) do |*args|
              scope = all._exec_scope(name, *args, &body)
              scope = scope.extending(extension) if extension
              scope
            end
          else
            singleton_class.define_method(name) do |*args|
              scope = body.call(*args) || all
              scope = scope.extending(extension) if extension
              scope
            end
          end

          generate_relation_method(name)
        end



코드의 주석에 따르면 named scope는 무슨 일이 있어도 ActiveRecord::Relation을 반환합니다. 따라서 nil이나 false가 나와야하는 경우에는 이 둘 대신에 model의 모든 record를 반환하게 됩니다.

 

맥락을 보아하니  scope = body.call(*args) || all 이 부분에서 nil을 model의 모든 record로 변환해 주고 있는 것 같습니다.

 

레일즈가 왜 이런 전략을 선택했는지는 잘 모르겠지만 사실 생각해보면 간단한 이슈였습니다. 애초에 'named scope는 nil을 반환하지 않는다'는 간단하지만 중요한 사실을 떠올릴 수 있었다면 named scope가 아니라 class method를 만들었겠죠.. 심지어 저는 몇주전에 이 내용에 대한 포스팅을 했었는데 헛공부 했네요...  

 

scope는 사실 내부적으로는 class method와 같지만 큰 차이점이 하나 있습니다. 바로 chaining이 가능 하다는 것입니다. 그말은 여러개의 scope를 연쇄시켜서 호출할 수 있다는 뜻인데요, 여러개의 scope들 중에 하나라도 nil이 포함되어 있다면 nil 참조 exception이 던져지겠죠. 때문에 scope는 nil을 반환할 수 없을 것 입니다. 그리고 각각의 scope를 연쇄시킬때 마다 데이터를 load 하는 것이 아니라 모든 조건식들을 한꺼번에 lazy loading을 하는 편이 경제적일 것입니다.

 

 

오늘의 교훈

named scope를 사용할때는 find, first, last 등으로 eager loading을 하고있지 않은지, 이때문에 nil이 반환되는 상황은 없는지 꼭 확인합시다! chaining이 필요없거나 eager loading이 필요한 상황이라면 class method를 사용하도록 합시다.

댓글