Search

Django ORM의 동작원리 (2)

이전글을 통해 Model.objects 가 어떻게 설정되는지 간단하게 알 수 있었다.
또한 objectsManager Class의 인스턴스임을 알 수 있었다.

Manager Class Deep-Dive

Manager Class란?

Manager는 Django 모델에 데이터베이스 쿼리 작업을 제공하는 인터페이스입니다.
Django 애플리케이션의 모든 모델에는 최소한 하나의 Manager가 존재합니다.
“최소한 하나의 Manager”가 존재하는 것은 이전글로 확인하였고, 이제 어떻게 “데이터베이스 쿼리 작업”을 제공하는지 알아보면 될 것 같다.

Manager Class의 기능 소개

아쉽게도 공식문서에는 Manager Class의 메서드를 일일히 소개해주진 않는다.
기능보다는, Manager 자체를 사용하는 다양한 방법(커스텀, 멀티 Manager 사용 등…)을 간단하게 보여준다.
이에 대한 내용은 기회가 되면 따로 글을 써봐야겠다.
먼저 Manager Class 선언 지점을 확인했다. 음…? 근데 별 내용이 없다.
BaseManager.from_queryset(QuerySet) 를 상속받는다니, 특이한 형태이다.
class Manager(BaseManager.from_queryset(QuerySet)): pass
Python
복사
BaseManager.from_queryset(QuerySet)Manager Class라고 할 수 있기 때문에, BaseManagerQuerySet 을 보는게 좋을 것 같다.

BaseManager

큰 내용은 없다.
정확히 말하면, Model 의 메타클래스였던 BaseModel 처럼 자체적인 동작을 한다기보다, QuerySet 에 대한 연결장치와 같은 느낌이다.
실제 사용되기도 했었던 from_queryset_get_queryset_methods 클래스 메서드를 확인해보자.
... @classmethod def _get_queryset_methods(cls, queryset_class): def create_method(name, method): @wraps(method) def manager_method(self, *args, **kwargs): return getattr(self.get_queryset(), name)(*args, **kwargs) return manager_method new_methods = {} for name, method in inspect.getmembers( queryset_class, predicate=inspect.isfunction ): # Only copy missing methods. if hasattr(cls, name): continue # Only copy public methods or methods with the attribute # queryset_only=False. queryset_only = getattr(method, "queryset_only", None) if queryset_only or (queryset_only is None and name.startswith("_")): continue # Copy the method onto the manager. new_methods[name] = create_method(name, method) return new_methods @classmethod def from_queryset(cls, queryset_class, class_name=None): if class_name is None: class_name = "%sFrom%s" % (cls.__name__, queryset_class.__name__) return type( class_name, (cls,), { "_queryset_class": queryset_class, **cls._get_queryset_methods(queryset_class), }, ) ...
Python
복사
두 메서드에서 QuerySet Class를 사용하여 현 BaseManager를 상속받고, QuerySet Class의 메서드를 설정해 새로운 Manager Class를 만드는 것으로 추정할 수 있다.
아래와 같은 Class 생성 방식이 이해가 어렵다면, 메타클래스에 대한 내용을 확인해보면 좋을 것 같다.
... return type( class_name, (cls,), { "_queryset_class": queryset_class, **cls._get_queryset_methods(queryset_class), }, ) ...
Python
복사

QuerySet

그렇다면 결국엔 우리가 Model.objects.get 으로 사용하던 getManager 클래스 자체에서 제공하는게 아닌, QuerySet Class의 기능임을 알 수 있다.
QuerySet의 정보를 한번 확인해보자.

get method

우리가 원하던 get 기능이 있는 것을 확인할 수 있다.
get 말고도, ORM에서 사용하던 많은 기능들은 QuerySet에서 선언된 기능임을 알 수 있다.
... def get(self, *args, **kwargs): """ Perform the query and return a single object matching the given keyword arguments. """ # 현재 combinator가 설정된 경우 Raise # union() / intersection() / difference() 사용 시 설정된다. if self.query.combinator and (args or kwargs): raise NotSupportedError( "Calling QuerySet.get(...) with filters after %s() is not " "supported." % self.query.combinator ) # filter 메서드를 통한 조건 설정 및 쿼리 진행 # filter에서는 Q() 객체를 통해 조건 설정 clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs) if self.query.can_filter() and not self.query.distinct_fields: clone = clone.order_by() limit = None if ( not clone.query.select_for_update or connections[clone.db].features.supports_select_for_update_with_limit ): limit = MAX_GET_RESULTS clone.query.set_limits(high=limit) num = len(clone) # 결과가 저장된 `_result_cache` 첫 데이터 호출 if num == 1: return clone._result_cache[0] # 미 존재할 경우 if not num: raise self.model.DoesNotExist( "%s matching query does not exist." % self.model._meta.object_name ) # get의 결과는 1개여야한다. raise self.model.MultipleObjectsReturned( "get() returned more than one %s -- it returned %s!" % ( self.model._meta.object_name, num if not limit or num < limit else "more than %s" % (limit - 1), ) ) ...
Python
복사
위에서 확인한 get 의 간단한 동작은 또 다른 QuerySet 객체를 Clone하여 필터링 및 order_by 설정 후 쿼리를 동작하는 방식인 것 같다.
settings.py 에 아래와 같은 설정을 추가하면, DB 쿼리 호출 시 간단하게 로깅이 가능하다.
DB 쿼리 호출 로깅 추가 코드
하지만, get 메서드 자체에는 실제 쿼리를 동작시키는 내용은 없는 것 같다. 분명 로깅은 되는데 이상했다.
확인을 위해 get 메서드 내 breakpoint 를 설정하여, 언제 실제 쿼리가 동작하는지 확인하였다.
확인 결과, len(clone) 을 통해 clone의 길이를 확인할 때 쿼리가 실행되었다.
자세한 확인을 위해, len() 사용 시 동작하는 __len__() 스페셜 메서드를 확인하였고 _fetch_all 이라는 메서드 동작 시 쿼리가 동작되는 것을 알 수 있었다.
class QuerySet(AltersData): ... def __len__(self): self._fetch_all() return len(self._result_cache) ...
Python
복사

_fetch_all method

이 메서드에서는 2가지 기능을 하는 것 같다.
get 메서드를 유심히 보았다면 알 수 있겠지만, QuerySet 의 결과는 _result_cache 에 저장된다.
_result_cache 를 설정하는게 이 메서드이다.
위 작업 말고도 prefetch 관련 작업도 하지만, 우선 Model.objects.get() 정도의 범위에서만 사용될 내용만 보자.
class QuerySet(AltersData): ... def _fetch_all(self): if self._result_cache is None: self._result_cache = list(self._iterable_class(self)) if self._prefetch_related_lookups and not self._prefetch_done: self._prefetch_related_objects() ...
Python
복사
결국에는 list(self._iterable_class(self)) 에서 동작하는 것인데, 이 한줄만 봐서는 모르겠다.
self._iterable_class(self)QuerySet._iterable_class(QuerySet) 형태라는 것인데, _iterable_class 의 확인이 필요할 것 같다.
확인해보니, ModelIterable 클래스를 또 사용한다. 더 깊이 확인해보자
class QuerySet(AltersData): """Represent a lazy database lookup for a set of objects.""" def __init__(self, model=None, query=None, using=None, hints=None): self.model = model self._db = using self._hints = hints or {} self._query = query or sql.Query(self.model) self._result_cache = None ... self._iterable_class = ModelIterable ... self._fields = None self._defer_next_filter = False self._deferred_filter = None ...
Python
복사
ModelIterable 클래스는 __iter__ 라는 스페셜 메서드가 선언되어 있다.
즉, list(self._iterable_class(self))list(ModelIterable(QuerySet)) 라고 할 수 있겠다.
자연스럽게 list(ModelIterable(QuerySet)) 동작 시 ModelIterable정의된 __iter__ 가 동작되며, execute_sql 가 동작되는 것이다.
class ModelIterable(BaseIterable): """Iterable that yields a model instance for each row.""" def __iter__(self): queryset = self.queryset db = queryset.db compiler = queryset.query.get_compiler(using=db) # Execute the query. This will also fill compiler.select, klass_info, # and annotations. results = compiler.execute_sql( chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size ) ...
Python
복사

간단한 Deep-Dive를 끝내며

물론 이번처럼 execute_sql 를 간접적으로 호출하며 쓰지 않는 경우도 많다.
QuerySet.update 의 경우 메서드에서 바로 complier의 execute_sql 을 쓰기도 한다.
class QuerySet(AltersData): """Represent a lazy database lookup for a set of objects.""" ... def update(self, **kwargs): """ Update all elements in the current QuerySet, setting all the given fields to the appropriate values. """ self._not_support_combined_queries("update") if self.query.is_sliced: raise TypeError("Cannot update a query once a slice has been taken.") self._for_write = True query = self.query.chain(sql.UpdateQuery) query.add_update_values(kwargs) ... # Clear any annotations so that they won't be present in subqueries. query.annotations = {} with transaction.mark_for_rollback_on_error(using=self.db): rows = query.get_compiler(self.db).execute_sql(CURSOR) self._result_cache = None return rows
Python
복사
적지 않은 내용이 생략되었지만, Django의 가장 대표적인 기능 중 하나인 ORM이 어떻게 동작하는지 알 수 있는 시간이었다.
그 동안은 간단하게 사용하였지만, SQL Query → ORM으로 변하기까지 생각보다 많은 절차들이 있다는 점을 잘 기억하면 좋을 것 같다.