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에 추가할 수 있습니다.
예를 들어 like
나 unlike
과 같은 특정 기능을 @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 방식은 상위 수준의 추상화를 해주다보니 점점 더 간단해지는 것 같으면서도…. 동시에 방법론이 여러개다 보니 복잡해지는 것 같기도 합니다…