DRF ViewSet

DRF CBV에는 APIView, GenericAPIView, Mixin, GenericView, ViewSet가 있습니다.
지난번에는 APIView를 공부해보았는데요.
그러다 보니 자연스레 다른 CBV에도 관심이 생기게 되었습니다.
그 중에서도 왠지 관심이 간 게 ViewSet…!
이번 포스트에서는 “DRF ViewSet” 은 무엇이며 어떻게 사용되는지 APIView 방법과 비교하며 정리해보았습니다.

APIView

View에서 APIView 사용하기

APIView는 들어오는 HTTP 요청을 처리하고 적절한 HTTP 응답을 반환하는 뷰의 기본 클래스입니다.

위의 FBV 코드를 APIView로 만들면 아래와 같습니다.

  • rest_framework.views로 부터 APIView를 import 합니다.
  • FBV 로직을 그대로 사용하되 get 메소드와 post 메소드 명시하면 끝 입니다.
from rest_framework.views import APIView

class ArticleListAPIView(APIView):
    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)


class ArticleDetailAPIView(APIView):

        # 두 번 이상 반복되는 로직은 함수로 빼면 좋습니다
    def get_object(self, pk):
        return get_object_or_404(Article, pk=pk)

    def get(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    def put(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article, data=request.data, partial=True)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data)

    def delete(self, request, pk):
        article = self.get_object(pk)
        article.delete()
        data = {"pk": f"{pk} is deleted."}
        return Response(data, status=status.HTTP_200_OK)

URL 패턴 정의하기

URL에서 어떤 view로 라우팅 할 것인지를 변경해줘야 합니다.

  • 기존에는 함수명을 사용해서 라우팅
  • CBV는 클래스명.as_view() 를 사용해서 라우팅
from django.urls import path
from . import views

app_name = "articles"
urlpatterns = [
    # 기존
    # path("", views.article_list, name="article_list"),
    # 변경
    path("", views.ArticleListAPIView.as_view(), name="article_list"),
]

ViewSets

View에서 ViewSet 사용하기

ViewSet은 여러 뷰들에 대한 논리를 단일 클래스로 결합하는 상위 수준의 추상화입니다.

여러 엔드포인트(endpoint)를 단일 클래스에서 관리할 수 있게 해줍니다.

이는 RESTful 원칙을 준수하고 일관된 URL 패턴이 필요한 API를 구축하는 데 특히 유용합니다.

ViewSet은 각 HTTP 메서드에 대해 자체적으로 액션 (action)을 정의합니다.

액션은 기본적인 CRUD 같은 프로세스를 함수로 정의했다고 생각하면 되는데요.

이렇게 함으로써 코드가 더 간결하게 만들 수 있습니다.

기본적인 ViewSet 사용법은 다음과 같습니다.

class ExampleViewSet(viewsets.ViewSet):
    """
    Example empty viewset demonstrating the standard
    actions that will be handled by a router class.

    If you're using format suffixes, make sure to also include
    the `format=None` keyword argument for each action.
    """

    def list(self, request):
        # 전체 데이터 가져오기
        pass

    def create(self, request):
        # 생성
        pass

    def retrieve(self, request, pk=None):
        # 하나의 데이터 가져오기
        pass

    def update(self, request, pk=None):
        # 수정
        pass

    def partial_update(self, request, pk=None):
        # 일부 수정
        pass

    def destroy(self, request, pk=None):
        # 삭제
        pass

위에서 APIView로 구현했던 로직을 그대로 ViewSet으로 변경하면 아래와 같습니다.

# views.py
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import action
from .serializers import ArticleSerializer
from .models import Articles


class ArticleViewSet(ViewSet):
    def get_object(self, pk):
        return get_object_or_404(Articles, pk=pk)

    def list(self, request):
        articles = Articles.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    def create(self, request):
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

    def retrieve(self, request, pk=None):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    def update(self, request, pk=None):
        article = self.get_object(pk)
        serializer = ArticleSerializer(
            article, data=request.data, partial=True)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data)

    def destroy(self, request, pk=None):
        article = self.get_object(pk)
        article.delete()
        data = {"pk": f"{pk} is deleted."}
        return Response(data, status=status.HTTP_200_OK)

URL 패턴 정의하기

Viewset은 하나의 클래스로 추상화 했기 때문에 urlpattern도 간단하게 입력할 수 있습니다.

update, detail, delete와 같이 pk가 필요한 경우에는 urls.py에서 지정한 urlpattern에서 뒤에 /pk/ 붙여주는 식으로 자동으로 urlpattern이 등록 됩니다.

# urls.py
from rest_framework.routers import DefaultRouter
from . import views

app_name = "articles"

# ViewSet을 Router에 등록
router = DefaultRouter()
router.register("", views.ArticleViewSet, basename='article')

# 추가 ViewSet이 있는 경우 해당 Viewset만 register해주면 됨
# router.register("comments", views.CommentViewSet, basename='comment')

# Router의 URL 패턴을 가져와서 urlpatterns에 추가
urlpatterns = router.urls

Note. PK가 아닌 Variable Routing이 필요한 경우

ViewSet에서 router를 사용하면 pk에 대한 variable routing까지 해주지만 그 외의 값은 추가적인 코드를 작성해야 하는 것 같습니다.

예를 들어 url에 username이 추가로 입력된 경우 해당 사용자의 모든 글을 가져오는 기능을 구현 하고 싶을 때 다음과 같이 urlpattern을 추가해줄 수 있습니다.

  • path를 추가해줍니다.
  • 첫번째 인자로는 variable routing에 들어갈 변수를 넣어줍니다.
  • 두번째 인자로는 ViewSet.as_view()에 어떤 method와 어떤 함수로 라우팅 할 것인지 명시해줍니다.
    • {‘get’:’retireve’} : 해당 URL에서 get method로 들어오는 요청은 ArticleViewSet의 retrieve 메소드로 넘기겠다는 의미입니다.
urlpatterns += [path('<str:username>/', views.ArticleViewSet.as_view({'get': 'retrieve'}), name='article'),]

@action : 사용자 지정 액션 추가

@action 데코레이터는 ViewSet 클래스의 메서드를 추가적인 액션으로 변환하는 데 사용됩니다.

이 데코레이터를 사용하면 기본적인 CRUD(Create, Retrieve, Update, Delete) 이외의 사용자 정의 액션을 ViewSet에 추가할 수 있습니다.

예를 들어 likeunlike과 같은 특정 기능을 @action을 통해서 구현할 수 있습니다.

from rest_framework.decorators import action

class ArticleViewSet(ViewSet):
    ...

    @action(detail=True, methods=['post'])
    def like(self, request, pk=None):
        article = self.get_object(pk)
        article.likes += 1
        article.save()
        return Response({'message': 'Article liked successfully'}, status=status.HTTP_200_OK)

위의 코드에서 @action 데코레이터는 like 메서드를 like 액션으로 변환합니다.

pk 값을 받고 작업하는 상세 작업

  • detail=True 인자를 넘겨줌으로써 이 액션은 개별 article에 대해 수행됨을 명시해 줄 수 있습니다.

요청 method 정의

  • methods=['post']인자를 넘겨줌으로써 HTTP POST 요청으로만 호출됩니다.
  • 따라서 개별 article에 대해 like 액션을 호출하려면 해당 article의 URL로 POST 요청을 보내야 합니다.

마치며

한번에 FBV, CBV 방법을 익히니 머리가 복잡해지는 것 같습니다… ㅎㅎ

CBV 방식은 상위 수준의 추상화를 해주다보니 점점 더 간단해지는 것 같으면서도…. 동시에 방법론이 여러개다 보니 복잡해지는 것 같기도 합니다…

참고하면 좋은 글

Leave a Comment

목차