ViewSet과 Router

guava·2022년 1월 15일
0

파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 강의를 듣고 정리한 글입니다.

ViewSet은 REST의 반복적인 코딩 패턴을 줄여준다.
우리는 일반적으로 REST API를 구현할 때 모델을 기준으로 List와 Detail URL에 대한 API를 구현한다.

이 때 List API는 GET, POST 메소드를 구현하며 Detail API 는 GET, PUT, DELETE메소드를 구현한다.

이 각 2개의 URL별로 모두 5개의 메소드 구현을 REST의 일관된 패턴으로 볼 수도 있을 것이다.

DRF에서는 모델을 기준으로 하나의 ViewSet으로 묶어서 위에서 언급한 패턴을 한방에 구현할 수 있다. ViewSet에 queryset과 serializer를 지정만 해주고 Router클래스로 url에 추가만 해주면 된다.

1. ViewSet


단일 리소스에서 관련있는 View들을 단일 클래스에서 제공

  • 2개의 URL이 필요하다. →list/create, detail/update/partial_update/delete와 같이 두개의 클래스 기반뷰가 필요하다.
  • ViewSet은 위 2개를 하나의 단일 클래스에서 지원한다.
  • ModelViewSet에서는 메소드가 전부 구현이 되어있고, 아래와 같이 메소드를 직접 구현이 가능하다.
# https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/
from rest_framework import viewsets
from rest_framework.response import Response

class PostViewSet(viewsets.ViewSet):
    def list(self, request):  # list
        queryset = Post.objects.all()
        serializer = PostSerializer(queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk):  # detail
        queryset = Post.objects.all()
        user = get_object_or_404(queryset, pk=pk)
        serializer = PostSerializer(user)
        return Response(serializer.data)

# https://www.django-rest-framework.org/api-guide/routers/
router = DefaultRouter()
router.register('post', PostViewSet, basename='post')  # 2개의 URL
router.urls

# urls에 다음과 같이 추가한다. -> path('', include(router.urls)

2. Post 리소스에 대한 2개의 URL


아래의 코드 역시 정형화된 패턴

⇒ ModelViewSet을 통해 간결하게 구현하실 수 있습니다.

  • 아래 두개의 Class를 ModelViewSet를 이용해서 하나의 클래스로 구현 가능
  • 보다 가독성 높은 코드로 관리성이 좋게 동일한 동작을 하는 API에 대한 고민이 언제나 필요하다.
from rest_framework import generics

class PostListAPIView(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

class PostDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

3. ModelViewSet


2가지 ModelViewSet

viewsets.ReadOnlyModelViewSet

  • list 지원 → 1개의 URL
  • detail 지원 → 1개의 URL

viewsets.ModelViewSet

  • list/create 지원 → 1개의 URL
  • detail/update/partial_update/delete 지원 → 1개의 URL

4. URL Patterns에 매핑하기


from rest_framework import viewsets
class PostViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

개별 View를 만들수도 있고

# PostViewSet으로부터 필요한것만 뽑아 쓰기

post_list = PostViewSet.as_view({
    'get': 'list',
})
post_detail = PostViewSet.as_view({
    'get': 'retrieve',
})

# urls에 직접 path를 다 써야한다.

Router를 통해 일괄적으로 urlpatterns에 등록하실 수 있다. (추천)

# Router를 통해 일괄등록을 하면 추가 기능이 있다. http://localhost:8000/post/1.api, http://localhost:8000/post/1.json 등의 포맷 인자(api, json)를 추가로 url_patterns에 추가된다. 포맷 인자로 요청 시 요청에 따라 json, api응답이 온다. 아래 Router 참조

from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('post', views.PostViewSet)  # 'post': URL에 사용될 리소스 이름이다. 보통은 모델명 사용

urlpatterns = [
    path('', include(router.urls)),
]

5. Router


ReadOnlyModelViewSet과 ModelViewSet에 대해서 동일한 URLPattern 리스트가 생성

  • list route
  • detail route
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('post', views.PostViewSet)

urlpatterns = [
    path('', include(**router.urls**)),
]
# ...
[
    <URLPattern '^post/$' [name='post-list']>, 
    <URLPattern '^post\.(?P<format>[a-z0-9]+)/?$' [name='post-list']>, # 주의) 구분자 "_"가 아니라 "-"이다.
    <URLPattern '^post/(?P<pk>[^/.]+)/$' [name='post-detail']>,
    <URLPattern '^post/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$' [name='post-detail']>,
    <URLPattern '^$' [name='api-root']>,
    <URLPattern '^\.(?P<format>[a-z0-9]+)/?$' [name='api-root']>  
# api-root : 현 Router에 등록된 ViewSet내역을 조회
]

6. ViewSet에 새로운 EndPoint 추가하기


from rest_framework.decorators import action

class PostModelViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

		"""
    URL Reverse 명: basename-함수명 (언더바는 하이픈으로 변경)
										즉, post-public이 된다.
		"""
    @action(detail=False, methods=['GET'])  # 특정 포스팅이 아니므로 detial=False
    def public(self, request):
        qs = self.get_queryset().filter(is_public=True)
        serializer = self.get_serializer(qs, many=True)
        return Response(serializer.data)

    @action(detail=True, methods=['PATCH']) # 특정 포스팅이므로 detail=True
    def set_public(self, request, pk):  # public으로 변경하는 api
        instance = self.get_object()
        instance.is_public = True
        instance.save()  # 또는 instance.save(update_fields=['is_public'])
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

    # @action의 detail인자에 따라서 list URL을 탈건지, detail URL을 탈건지 결정된다.

생성 된 URL Pattern 리스트

[
    # ...
    <URLPattern	'^post/public/$' [name='post-public']>,
    <URLPattern	'^post/public\.(?P<format>[a-z0-9]+)/?$' [name='post-public']>,
    # ...
    <URLPattern	'^post/(?P<pk>[^/.]+)/set_public/$' [name='post-set-public']>,
    <URLPattern	'^post/(?P<pk>[^/.]+)/set_public\.(?P<format>[a-z0-9]+)/?$' [name='post-set-public']>,
    # ...
]

HTTPie를 활용한 요청의 예

  • PUT 요청: 반영할 모든 필드값을 지정
  • PATCH 요청: 변경할 값만을 지정
http PATCH http://도메인/post/10/set_public/
http PATCH http://도메인/post/10/ is_public=true  # PATCH요청 시 ModelViewSet에 구현되어 있는 partial_update로직을 통해서 업데이트 된다.

0개의 댓글