본문 바로가기
Web/Django

[DRF] 3. ViewSet / Router

by 조엘 2021. 1. 14.

안녕하세요! 파피몬입니다! 🎇

백엔드 API 서버가 어떻게 구축되는지 조금 알 필요가 있다고 생각이 들어서 Django Rest Framework(이하 DRF)를 공부하고 있습니다. 아직 개발자를 지망하는 학생입니다! 틀린 부분이 있으면 댓글로 알려주시면 정말 감사하겠습니다!!

 

 

*** 개요 ***

앞선 포스팅(참고: papimon.tistory.com/68)에서 DRF의 view를 어찌 CBV 방식으로 작성하는지 공부했다. 해당 방식으로 코드를 작성하면 또 하나의 중복이 나타나는데, 바로 데이터 목록디테일 데이터에 대한 클래스를 각각 구현해 주어야 한다는 점이다. 포스팅 마지막에서 바라본 Generic CBV 같은 경우, 

# views.py
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    
    
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

 

위와 같이 Snippet에 대한 데이터 목록디테일 데이터에 대한 클래스를 별도로 구현했다. 

DRF는 ViewSet을 통해 한 층 더 추상화된 구현을 가능하게 한다. 한 번 알아보도록 하자 :) 

 

 

*** ViewSet ***

DRF의 공식 문서(참고: www.django-rest-framework.org/tutorial/6-viewsets-and-routers/)를 살펴보면, ViewSet을 다음과 같이 소개하고 있다. 본 포스팅에 사용되는 코드 예시들은 모두 공식 문서에서 가져왔다. 

 

REST framework includes an abstraction for dealing with ViewSets, that allows the developer to concentrate on modeling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.

ViewSet을 통해 개발자는 API의 상태와 기능 구현에 더 힘쓸 수 있다. URL 만드는 것은 자주 쓰이는 관례에 따라 자동적으로 처리해준다. (아래 언급할 Router를 통해 뚝딱 처리 가능)

 

ViewSet은 총 4가지 ViewSet / GenericView / ReadOnlyViewSet / ModelViewSet 버전을 제공하는데, 해당 포스팅에서는 ReadOnlyViewSet과 ModelViewSet을 다뤄보도록 하겠다. 

 

ReadOnlyViewSet

해당 클래스는 mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet을 상속받은 클래스이다.

GenericViewSetgenerics.GenericAPIView를 상속받은 클래스로, 쿼리셋과 직렬화 클래스 등록을 담당한다. 

따라서 ReadOnlyViewSet에서는 GET 방식의 list/retrieve, 즉 해당 데이터의 조회만 지원한다. 

# views.py
from rest_framework import viewsets

class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    This viewset automatically provides `list` and `retrieve` actions.
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

 

해당 코드를 보면 이전 포스팅과 다르게 데이터 목록과 디테일 데이터에 대한 클래스를 구분하지 않은 것을 볼 수 있다. 오로지 ReadOnlyModelViewSet을 상속받은 클래스에 쿼리셋과 직렬화 클래스만 등록해 주었다.

 

이것이 가능한 이유는 데이터 목록 - 디테일 데이터 로직이 정형화되어 추상화가 가능하기 때문이다. 아래에서 살펴볼 router를 이해하면, 해당 클래스의 list와 retrieve 함수가 어떻게 호출되는지 이해할 수 있다. 

 

ModelViewSet

해당 클래스는 mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet을 상속받은 클래스이다. 

따라서 ModelViewSet은 앞선 포스팅에서 살펴본 CRUD 로직을 손쉽게 제공해 준다. 

 

추가적인 고려사항은 CRUD가 아닌 로직의 처리인데, 이는 @action 데코레이터로 해결이 가능하다. 

action decorator

@action 데코레이터를 통해서 앞선 포스팅에서 살펴본 APIView의 방식처럼 직접 어떤 요청에 따른 어떤 Response와 status를 응답할지 작성해주면 된다. 

 

# views.py
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import permissions

class SnippetViewSet(viewsets.ModelViewSet):
    """
    This viewset automatically provides `list`, `create`, `retrieve`,
    `update` and `destroy` actions.

    Additionally we also provide an extra `highlight` action.
    """
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

 

위의 설명처럼 쿼리셋과 직렬화 클래스의 등록만으로 데이터 목록 - 디테일 데이터에 대한 CRUD 구현을 완료하였다. 

@action 데코레이터로 highlight 라는 함수를 정의했다. 참고로 해당 함수를 호출하기 위해서는 해당 함수 이름을 url의 마지막에 추가해 요청을 보내야한다. 

- detail = True 라면, detail url 뒤에 덧붙여 "~/snippet/1/highlight" 처럼

- detail = False 라면, list url 뒤에 덧붙여 "~/snippet/highlight" 처럼

 

또한 CRUD를 누구나 맘대로 조작하면 큰일나기 때문에 permission_classes에 권한 자격을 명시해 주었다. 이는 차후 포스팅에서 더 깊게 다루도록 하겠다. 

 

 

*** Router ***

위에서 언급한 바와 같이 ViewSet은 List와 Detail에 대한 정보를 둘 다 추상화 시킨다. 따라서, urls.py는 path 함수 하나만 가지고 List와 Detail 둘 모두 제어하기란 불가능하다.

고로, 아래와 같이 어떤 url 요청이 어떤 method로 들어오면, ViewSet에서 해당 method에 대응되는 함수를 호출하도록 as_view를 코딩해야 한다.  

# urls.py
from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers

snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
    'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
    'get': 'list'
})
user_detail = UserViewSet.as_view({
    'get': 'retrieve'
})

urlpatterns = format_suffix_patterns([
    path('', api_root),
    path('snippets/', snippet_list, name='snippet-list'),
    path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
    path('users/', user_list, name='user-list'),
    path('users/<int:pk>/', user_detail, name='user-detail')
])

 

하지만... 보다 싶이 굉장히 장황하다. 모델이 추가될 수록 더더욱 장황해 질 것이다. 따라서 DRF에서는 Router를 지원하는데, 위와 같은 정형화된 URL configuration을 알아서 만들도록 추상화 시켜두었다. 

 

# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views

# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)

# The API URLs are now determined automatically by the router.
urlpatterns = [
    path('', include(router.urls)),
]

 

DefaultRouter의 객체를 생성하여 ViewSet을 등록하면 정형화된 URL Configuration이 완료된다!

 

 

반응형

댓글