Django REST framework - Generic views

Hoonkii·2022년 1월 2일
1

Django

목록 보기
1/2
post-custom-banner

Generic views

오늘은 사내 API 개발 중 가장 많이 사용되고 있는 Django REST framework의 generic 뷰에 대해서 다루어 보려고 한다.

기본적으로 장고에는 class-based 뷰function-based 뷰가 존재하는데, generic view는 class-based 뷰 이며 자주 사용되는 REST API 개발을 빠르게 하게 도와준다.

Generic views는 자주 사용되는 패턴이나 idom들을 갖추어 여러 뷰들을 개발할 때 무의미한 반복을 줄일 수 있게 해준다.

그러면 이제 generic 뷰의 패키지를 까보자.

from rest_framework import generics

제네릭 뷰의 패키지는 위와 같으며, generics 패키지 안 소스코드를 확인해보면 CreateAPIView, ListAPIView, UpdateAPIView등등 엄청나게 많은 View들이 존재한다. 이 View들은 기본적으로 여러 모델 Mixin과 하나의 GenericAPIView를 상속하고 있다.

하나의 예시 코드를 보자.

class ListCreateAPIView(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        GenericAPIView):
    """
    Concrete view for listing a queryset or creating a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

위 코드는 generics 패키지에 있는 뷰들 중 하나인 ListCreateAPIView이다. 이 뷰는 두 개의 Mixin과 하나의 GenericAPIView를 상속받고 있다. get, post 함수가 존재하며 상위 mixin의 함수를 이용해 get, post함수가 구현되어 있다.

그러면 모든 뷰들이 공통적으로 상속하고 있는 GenericAPIView 코드를 분석해보자.

class GenericAPIView(views.APIView):
    """
    Base class for all other generic views.
    """
   
    queryset = None
    serializer_class = None
    lookup_field = 'pk'
    lookup_url_kwarg = None

    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    def get_queryset(self):
        """
        Get the list of items for this view.
        This must be an iterable, and may be a queryset.
        Defaults to using `self.queryset`.

        This method should always be used rather than accessing `self.queryset`
        directly, as `self.queryset` gets evaluated only once, and those results
        are cached for all subsequent requests.

        You may want to override this if you need to provide different
        querysets depending on the incoming request.

        (Eg. return a list of items that is specific to the user)
        """
        assert self.queryset is not None, (
            "'%s' should either include a `queryset` attribute, "
            "or override the `get_queryset()` method."
            % self.__class__.__name__
        )

        queryset = self.queryset
        if isinstance(queryset, QuerySet):
            # Ensure queryset is re-evaluated on each request.
            queryset = queryset.all()
        return queryset

    def get_object(self):
        """
        Returns the object the view is displaying.

        You may want to override this if you need to provide non-standard
        queryset lookups.  Eg if objects are referenced using multiple
        keyword arguments in the url conf.
        """
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)

        return obj

    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        return serializer_class(*args, **kwargs)

    def get_serializer_class(self):
        """
        Return the class to use for the serializer.
        Defaults to using `self.serializer_class`.

        You may want to override this if you need to provide different
        serializations depending on the incoming request.

        (Eg. admins get full serialization, others get basic serialization)
        """
        assert self.serializer_class is not None, (
            "'%s' should either include a `serializer_class` attribute, "
            "or override the `get_serializer_class()` method."
            % self.__class__.__name__
        )

        return self.serializer_class

    def get_serializer_context(self):
        """
        Extra context provided to the serializer class.
        """
        return {
            'request': self.request,
            'format': self.format_kwarg,
            'view': self
        }

    def filter_queryset(self, queryset):
        """
        Given a queryset, filter it with whichever filter backend is in use.

        You are unlikely to want to override this method, although you may need
        to call it either from a list view, or from a custom `get_object`
        method if you want to apply the configured filtering backend to the
        default queryset.
        """
        for backend in list(self.filter_backends):
            queryset = backend().filter_queryset(self.request, queryset, self)
        return queryset

    @property
    def paginator(self):
        """
        The paginator instance associated with the view, or `None`.
        """
        if not hasattr(self, '_paginator'):
            if self.pagination_class is None:
                self._paginator = None
            else:
                self._paginator = self.pagination_class()
        return self._paginator

    def paginate_queryset(self, queryset):
        """
        Return a single page of results, or `None` if pagination is disabled.
        """
        if self.paginator is None:
            return None
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

    def get_paginated_response(self, data):

여러 함수들이 있는데, 그 중 주목해볼 것은 get_queryset(), get_object(), get_serializer()이다.

REST API를 설계 및 개발하다 보면, path variable을 통해 하나의 모델 객체에 대한 액션을 구성할 때도 있고, 객체를 생성하거나 객체의 list를 불러오는 액션을 구성할 수 도 있다.

하나의 모델 객체에 대해 조회, 삭제, 업데이트를 구성하기 위해서는 먼저 모댈 객체를 조회해야하는 데 이 때 사용되는 GenericAPIView의 함수가 get_object()이다. 이는 개발자가 GenericAPIView를 상속한 뷰(예를 들어 RetrieveAPIView)를 사용할 때 오버라이딩하여 원하는 객체를 로드하게 할 수 있다.

어떤 모델의 목록들을 불러와야 하는 경우는 Django에서는 queryset 형태로 불러오게 되는데 이 때 사용되는 GenericAPIView의 함수가 get_queryset()이다.

HTTP Body는 파이썬 모델 객체로 변환되거나, 모델 객체는 Json 으로 표현되어야 하는데 이 역할을 수행하는 것이 serializer이며, get_serializer()에 serializer를 지정함으로 모델 객체를 어떻게 받고 검증할 것인지, 혹은 어떻게 Json 형태로 표현할 것인지를 지정할 수 있다.


이제 그러면 이 함수들이 어떻게 Mixin들에서 사용되는지 알아보자.

모델 객체의 단일 조회, 목록 조회를 지원하기 위해서 제공하는 제네릭 뷰는 아래와 같다.

class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    """
    Concrete view for retrieving a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    """
    Concrete view for listing a queryset.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

각 뷰들은 mixins 패키지의 mixin들을 각각 상속받고, GenericAPiView를 상속받는다. 그럼 상위 Mixin 코드를 살펴보자.

class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

class ListModelMixin:
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

우와! GenericAPIViews에 정의된 함수들이 그대로 사용되고 있다.

RetrieveModelMixin의 경우 코드를 살펴보면, get_object() 함수를 통해 명시된 단일 객체를 불러오고, serializer에 단일 객체를 지정한 다음, Http 응답을 리턴한다.

ListModelMixin의 경우는, 먼저 queryset을 불러온 뒤 필터링을 한다음, 페이지네이션을 적용하고 (혹은 없으면 적용하지 않고) serializer의 many=True 옵션을 준 다음 Http 응답을 리턴한다.

그러면 이걸 어떻게 사용할 수 있을까?


예를 들어 generics의 RetrieveUpdateDestroyView를 사용해서 RUD API를 구현한다고 하면...

from rest_framework import generics

class ClassDetailView(generics.RetrieveUpdateAPIView):

    serializer_class = EasyClassSerializer

    def get_queryset(self):
        return EasyClass.objects.get(id=self.kwargs["pk"])

짜잔 이런식으로 구현하면 끝이다. get, put, patch, delete에 대한 액션은 RetrieveUpdateAPIView에 다 구현되어 있기 때문에 serializer_class와 get_queryset 인터페이스만 구현되면 쉽게 API가 완성된다. (물론 프로덕션 개발할 때는 여러 요구사항 때문에 커스텀 해야할게 많지만..)


generics 패키지를 살펴 보던 중 의아한 점이 있었는데 CRUD의 Mixin을 모두 상속받아 제공하는 View가 없었다. 곰곰이 생각해보면 이는 당연한 것인게, Class-based 뷰는 한 url에 대해 매핑된다. 그러면 다음과 같은 형식으로 매핑이 될 것이다.

path("classes/<int:pk>", ClassDetailView.as_view()),
path("classes", CourseCreateList.as_view()),

그러면 결국 id로 조회해서 단일 오브젝트에 대해서 가능한 액션들을 포함한 뷰와, 여러 오브젝트 (list) 혹은 생성하는 액션들은 분리될 수 밖에 없다. 그래서 CRUD가 다 포함된 뷰는 generics에서 따로 정의되지 않았다.

생각한 점..

Generic 뷰가 거의 모든 것을 다 해주다보니... 이 프레임워크를 처음 쓰는 사람 입장에서 이게 대체 어떻게 되지? 라는 생각을 할 수 있을 것 같다. (내가 처음에 그랬다..)
Django Generic 뷰를 커스터마이징 할 일이 많아짐에 따라 내부 구조를 구조를 이해하고 있어야 커스터마이징하기 쉽고 불필요한 코드 재사용이 없어진다는 것을 느꼈다. 앞으로 항상 프레임워크의 동작 원리를 꼼꼼히 분석할 것이다.

profile
개발 공부 내용 정리
post-custom-banner

0개의 댓글