라우팅을 통해 요청에 사용할 컨트롤러가 결정되면, 컨트롤러는 요청을 처리하고 적절한 출력을 생성하는 역할을 담당합니다.
— Ruby on Rails 문서
Django REST 프레임워크에서는 관련된 여러 뷰의 로직을 하나의 클래스인 ViewSet으로 결합할 수 있습니다. 다른 프레임워크에서는 이를 ‘리소스(Resources)’ 또는 ‘컨트롤러(Controllers)’라고 부르기도 합니다.
ViewSet 클래스는 클래스 기반 뷰의 한 유형으로, .get() 또는 .post()와 같은 메서드 핸들러를 제공하지 않고, 대신 .list() 또는 .create()와 같은 액션을 제공합니다.
뷰셋의 메서드 핸들러는 .as_view() 메서드를 사용해 뷰를 최종화할 때 해당 액션에 바인딩됩니다.
일반적으로 URL 설정 파일에서 뷰셋의 뷰를 명시적으로 등록하는 대신, 라우터 클래스에 뷰셋을 등록하여 자동으로 URL 구성을 생성합니다.
다음은 시스템의 모든 사용자를 나열하거나 조회할 수 있는 간단한 뷰셋을 정의한 예시입니다.
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from myapps.serializers import UserSerializer
from rest_framework import viewsets
from rest_framework.response import Response
class UserViewSet(viewsets.ViewSet):
"""
사용자 목록을 나열하거나 조회하는 간단한 뷰셋입니다.
"""
def list(self, request):
queryset = User.objects.all()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
serializer = UserSerializer(user)
return Response(serializer.data)
필요에 따라 다음과 같이 이 뷰셋을 두 개의 별도 뷰로 바인딩할 수 있습니다.
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})
보통 이렇게 하지는 않으며, 대신 뷰셋을 라우터에 등록하고 URL 구성을 자동으로 생성합니다.
from myapp.views import UserViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = router.urls
직접 뷰셋을 작성하는 대신, 기본적인 동작을 제공하는 기존의 기본 클래스를 사용하는 경우가 많습니다. 예를 들어:
class UserViewSet(viewsets.ModelViewSet):
"""
사용자 인스턴스를 조회하고 수정할 수 있는 뷰셋입니다.
"""
serializer_class = UserSerializer
queryset = User.objects.all()
뷰셋을 사용하는 주된 장점은 두 가지입니다.
하지만 이러한 장점에는 일장일단이 있습니다. 정규 뷰와 URL 설정을 사용하는 방식이 더 명확하고 제어권을 제공합니다. 뷰셋은 빠르게 개발하거나, 큰 API에서 일관된 URL 구성을 적용하려 할 때 유용합니다.
REST 프레임워크에서 기본 제공하는 라우터는 아래와 같이 표준적인 생성(create), 조회(retrieve), 수정(update), 삭제(destroy) 스타일의 액션에 대한 경로를 제공합니다.
class UserViewSet(viewsets.ViewSet):
"""
라우터 클래스가 처리할 표준 액션을 보여주는 빈 뷰셋 예시입니다.
형식 접미사를 사용할 경우 각 액션에 `format=None` 키워드 인수를 포함하세요.
"""
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
디스패치 시 뷰셋에는 다음과 같은 속성이 사용 가능합니다.
list, create).detail 속성과 연동.이 속성들을 확인해 현재 액션에 따라 동작을 조정할 수 있습니다. 예를 들어, 다음과 같이 list 액션을 제외한 모든 액션에 대해 권한을 제한할 수 있습니다.
def get_permissions(self):
"""
이 뷰에 필요한 권한 목록을 인스턴스화하고 반환합니다.
"""
if self.action == 'list':
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
경우에 따라 라우팅 가능한 임시 메서드가 필요할 수 있습니다. 이때 @action 데코레이터를 사용해 해당 메서드를 라우팅할 수 있습니다. 일반 액션과 마찬가지로 추가 액션은 단일 객체 또는 전체 컬렉션을 대상으로 할 수 있습니다. 이를 표시하려면 detail 인수를 True 또는 False로 설정합니다. 라우터는 이에 따라 URL 패턴을 구성합니다. 예를 들어, DefaultRouter는 상세 액션이 URL 패턴에서 pk를 포함하도록 설정합니다.
좀 더 완성된 추가 액션 예시는 다음과 같습니다.
from django.contrib.auth.models import User
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
표준 액션을 제공하는 뷰셋입니다.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
@action(detail=True, methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.validated_data['password'])
user.save()
return Response({'status': 'password set'})
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
@action(detail=False)
def recent_users(self, request):
recent_users = User.objects.all().order_by('-last_login')
page = self.paginate_queryset(recent_users)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(recent_users, many=True)
return Response(serializer.data)
@action 데코레이터는 기본적으로 GET 요청을 라우팅하지만, methods 인수를 설정하여 다른 HTTP 메서드를 지정할 수도 있습니다. 예를 들어:
@action(detail=True, methods=['post', 'delete'])
def unset_password(self, request, pk=None):
...
methods 인수는 HTTPMethod로 정의된 HTTP 메서드도 지원합니다. 아래 예시는 위의 예시와 동일합니다.
from http import HTTPMethod
@action(detail=True, methods=[HTTPMethod.POST, HTTPMethod.DELETE])
def unset_password(self, request, pk=None):
...
이 데코레이터는 permission_classes, serializer_class, filter_backends와 같은 뷰셋 레벨 구성을 재정의할 수 있습니다. 예를 들어:
@action(detail=True, methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
새로 추가된 두 액션은 URL 패턴 ^users/{pk}/set_password/$ 및 ^users/{pk}/unset_password/$에 따라 사용 가능합니다. url_path 및 url_name 매개변수를 사용하여 URL 세그먼트 및 액션의 리버스 URL 이름을 변경할 수 있습니다.
모든 추가 액션을 보려면 .get_extra_actions() 메서드를 호출하십시오.
추가 액션은 개별 뷰셋 메서드로 별도의 HTTP 메서드를 매핑할 수 있습니다. 예를 들어, 위의 비밀번호 설정/해제 메서드를 하나의 경로로 통합할 수 있습니다. 추가 매핑은 인수를 받지 않는다는 점에 유의하십시오.
@action(detail=True, methods=["put"], name="Change Password")
def password(self, request, pk=None):
"""사용자의 비밀번호를 업데이트합니다."""
...
@password.mapping.delete
def delete_password(self, request, pk=None):
"""사용자의 비밀번호를 삭제합니다."""
...
액션의 URL이 필요한 경우, .reverse_action() 메서드를 사용하세요. 이 메서드는 reverse()에 대한 편의 래퍼로, 뷰의 요청 객체를 자동으로 전달하며 URL 이름 앞에 basename 속성을 추가합니다.
basename은 뷰셋을 등록할 때 라우터가 제공하며, 라우터를 사용하지 않을 경우 .as_view() 메서드에 basename 인수를 직접 제공해야 합니다.
이전 섹션의 예시를 사용하면:
>>> view.reverse_action("set-password", args=["1"])
'http://localhost:8000/api/users/1/set_password'
또한 @action 데코레이터에 의해 설정된 url_name 속성을 사용할 수도 있습니다.
>>> view.reverse_action(view.set_password.url_name, args=['1'])
'http://localhost:8000/api/users/1/set_password'
.reverse_action()의 url_name 인수는 @action 데코레이터의 동일한 인수와 일치해야 합니다. 이 메서드는 list, create와 같은 기본 액션을 역전하는 데도 사용할 수 있습니다.
ViewSet 클래스는 APIView로부터 상속됩니다. 뷰셋에서는 permission_classes, authentication_classes와 같은 표준 속성을 사용하여 API 정책을 제어할 수 있습니다.
ViewSet 클래스는 액션의 구현을 제공하지 않으므로, 클래스를 상속받아 명시적으로 액션 구현을 정의해야 합니다.
GenericViewSet 클래스는 GenericAPIView를 상속받으며, get_object, get_queryset 등의 메서드를 포함한 기본 동작을 제공합니다. 그러나 기본적으로는 액션을 포함하지 않습니다.
GenericViewSet 클래스를 사용하려면 클래스를 상속받아 필요한 믹스인 클래스를 추가하거나 액션 구현을 명시적으로 정의해야 합니다.
ModelViewSet 클래스는 GenericAPIView를 상속받으며, 여러 믹스인 클래스의 동작을 혼합하여 다양한 액션에 대한 구현을 포함합니다.
ModelViewSet 클래스에서 제공하는 액션은 .list(), .retrieve(), .create(), .update(), .partial_update(), .destroy()입니다.
예제
ModelViewSet은 GenericAPIView를 확장하므로, 일반적으로 queryset 및 serializer_class 속성을 제공해야 합니다. 예를 들어:
class AccountViewSet(viewsets.ModelViewSet):
"""
계정 정보를 조회하고 수정할 수 있는 간단한 뷰셋입니다.
"""
queryset = Account.objects.all()
serializer_class = AccountSerializer
permission_classes = [IsAccountAdminOrReadOnly]
GenericAPIView에서 제공하는 표준 속성이나 메서드 재정의를 사용할 수 있습니다. 예를 들어, 동적으로 쿼리셋을 결정하는 뷰셋을 사용하려면 다음과 같은 방식으로 구현할 수 있습니다.
class AccountViewSet(viewsets.ModelViewSet):
"""
사용자가 소유한 계정 정보를 조회하고 수정할 수 있는 간단한 뷰셋입니다.
"""
serializer_class = AccountSerializer
permission_classes = [IsAccountAdminOrReadOnly]
def get_queryset(self):
return self.request.user.accounts.all()
그러나 뷰셋에서 queryset 속성을 제거하면, 관련된 라우터는 basename을 자동으로 유추할 수 없게 되며, 라우터 등록 시 basename 인수를 명시적으로 지정해야 합니다.
또한 이 클래스는 기본적으로 생성, 목록, 조회, 수정, 삭제 액션을 완전히 제공하지만, 표준 권한 클래스를 사용하여 사용 가능한 작업을 제한할 수 있습니다.
ReadOnlyModelViewSet 클래스도 GenericAPIView를 상속받습니다. ModelViewSet과 마찬가지로 여러 액션에 대한 구현을 포함하지만, list() 및 retrieve()와 같은 '읽기 전용' 액션만 제공합니다.
예제
ModelViewSet과 마찬가지로, 일반적으로 queryset 및 serializer_class 속성을 제공해야 합니다. 예를 들어:
class AccountViewSet(viewsets.ReadOnlyModelViewSet):
"""
계정 정보를 조회할 수 있는 간단한 뷰셋입니다.
"""
queryset = Account.objects.all()
serializer_class = AccountSerializer
마찬가지로 GenericAPIView에서 제공하는 표준 속성 및 메서드 재정의를 사용할 수 있습니다.
전체 ModelViewSet 액션 세트를 제공하지 않거나, 동작을 커스터마이징하는 커스텀 뷰셋 클래스를 제공해야 할 수 있습니다.
GenericViewSet 클래스로부터 상속되고, create, list, 그리고 retrieve 동작을 제공하는 기본 뷰셋 클래스와 믹스인을 만들기 위한 코드입니다.
from rest_framework import mixins, viewsets
class CreateListRetrieveViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
"""
retrive, create, list 동작을 제공하는 뷰셋입니다.
클래스를 오버라이딩하고 .queryset과 .serializer_class 속성을 설정하여 이 뷰셋을 사용할 수 있습니다.
"""
pass
커스텀 기본 뷰셋 클래스를 만듦으로써, 당신은 여러 API 뷰셋들에 걸쳐 재사용될 수 있는 공통 동작을 만들 수 있습니다.