[DRF] <Level Three> Django REST Framework - 5. Viewsets and Routers

Alex of the year 2020 & 2021·2020년 9월 3일
0

Django Rest Framework

목록 보기
15/15
post-thumbnail

Viewsets & Routers

Viewset

Viewset classes는 이름 그대로 연관된 view들을 한 class로 묶어주는 역할을 한다. 이를테면 한 queryset에서 list of elements를 보여주는 뷰와 같은 모델에서 한 instance의 detail을 보여주는 뷰를 한 클래스에 묶는 것이다. 따라서 여태까지 봐왔던 여느 APIViews보다 고수준의 abstraction level에 해당하고 사용하기 편리하기도하지만 그만큼 이해도가 높아야 사용할 수 있다.

ViewSets 역시 Class Based View에 해당한다. 따라서 handler method인 .get()이나 .post를 direct하게 사용하지 않고, action method인 .list(), .create()를 사용하게 된다.

Routers

이런 viewset을 더욱 강력하게 해주는 것이 Router class와의 combination이다. Router는 각각의 다른 actions에 대해 그에 맞는 적절한 url path를 router 내의 convention에 따라 자동으로 연결해준다.

Code

1) api/views.py 부터 Viewset에 맞게 수정해보자.
수정 전, 지금까지의 views.py를 다시 보면서 왜 Viewset형식의 views.py를 선호하는지 알아보자.

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from profiles.models import Profile
from profiles.api.serializers import ProfileSerializer

class ProfileList(generics.ListAPIView):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
    permission_classes = [IsAuthenticated]

이 코드의 단점은, ProfileList 클래스를 만들어도, ProfileDetail 클래스를 만들어도 모두 그 아래에 해당하는 queryset, serializer_class가 동일하다는 것이다. 즉 같은 자원에 대한 클래스가 두 개가 생기면 같은 코드를 그만큼 써야하는 비효율성이 존재하게 된다. 하지만 Viewset은 이러한 비효율성을 용납하지 않는다. Viewset을 이용한 코드는 아래에 있다.

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet # 1) 읽기전용의 Viewsest을 임포트한다

from profiles.models import Profile
from profiles.api.serializers import ProfileSerializer

class ProfileViewSet(ReadOnlyModelViewSet): # 2) class 이름과 상속받는 클래스 이름도 그에 맞추어 변경한다.
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
    permission_classes = [IsAuthenticated]
    # 3) 기본 세팅에 해당하는 위의 세 줄은 동일하게 가져간다.

2) api/urls.py 도 views.py의 변화에 맞추어 수정하자.

from django.urls import path
from profiles.api.views import ProfileViewSet # Class 이름 ViewSet으로 변경

# 위에까지만 써도 괜찮지만, 정확히 이번 엔드포인트가 어떤 기능을 담당하는지를 명시해주기 위해
# 아래의 변수를 선언해준다. profile_list라고 적어 ListView에 해당하는 엔드포인트임을 알 수 있다. 
# 그리고 as_view()의 인자값으로는 어떠한 메소드와 액션을 취할 것인지 dictionary형으로 적어줘야 한다.
profile_list = ProfileViewSet.as_view({"get": "list"})
profile_detail = ProfileViewSet.as_view({"get": "retrieve"})

urlpatterns = [
	path("profiles/", profile_list, name="profile-list"),
    	path("profiles/<int:pk>/", profile_detail, name="profile-detail"),
]

3) runserver 후, 127.0.0.1:8000/api/profiles/에 접속하여, admin 계정으로 로그인한다. profile-list에 해당하는 엔드포인트로 접속했기에 프로필 리스트가 나온다.

127.0.0.1:8000/api/profiles/3 로도 접속해본다. id=3에 해당하는 profile만 보이는 detail 페이지가 로드된다.

한 클래스로 두 개의 엔드포인트를 성공적으로 로드하는 것을 확인했다.

views.py에서 임포트했던 ReadOnlyModelViewSet은, list()와 retrieve() actions를 제공하기에 다음과 같은 결과가 성공적으로 가능한 것이다.

4) 이번엔 위에서 성공적으로 작동했던 api/urls.py를 Router를 이용하여 좀 더 효율적으로 수정해보자.

from django.urls import path
from rest_framework.routers import DefaultRouter # Router를 사용하기 위해서 임포트
from profiles.api.views import ProfileViewSet 

# profile_list = ProfileViewSet.as_view({"get": "list"})
# profile_detail = ProfileViewSet.as_view({"get": "retrieve"})
# 한 클래스 내에서 각자 다른 endpoint로 향하기 위해 메소드와 액션을 묶어 인자로 넣어 
# 지정했던 변수들은 잠시 주석처리 한다. 

router = DefaultRouter() # 임포트한 DefaltRouter 클래스의 인스턴스 router
router.register(r"profiles", ProfileViewSet) 
# router에 profiles 엔드포인트 등록, 해당 엔드포인트가 사용할 ViewSet 지정

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

이후 runserver, 바로 127.0.0.1:8000/api/profiles/3 으로 들어간다.
아까와 동일한 detail 화면이 보인다. 다만 web page의 변화가 눈에 띈다.

아래의 화면이 router 설정 전 화면이라면

아래 화면은 router 설정 후의 화면이다.

Router 설정 후 변화

  1. 상단의 Profile / Profile 이 Api Root / Profile List / Profile Instance로 더욱 더 현재 페이지의 속성을 알기 쉽게 바뀌었다. (Api Root는 우리가 임포트한 DefaultRouter에 해당하는 페이지이다. 클릭 시, profiles에 해당하는 엔드포인트 주소가 Json 형태로 주어져있다.)

  2. 이 페이지는 아까 Profile이라는 이름으로 존재했는데, router 설정 이후
    Profile Instance로 그 페이지 명이 바뀌었다.


5) api/views.py
이번에는 Update까지 가능하도록 만들어보자.

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated

# from rest_framework.viewsets import ReadOnlyModelViewSet
# Update를 하려면 ReadOnlyModelViewSet으로는 부족하여 아래와 같이 좀더 광범위하게 임포트한다.

from rest_framework import viewsets
from rest_framework import mixins

from profiles.models import Profile
from profiles.api.serializers import ProfileSerializer

class ProfileViewSet(mixins.UpdateModelMixin,
		     mixins.ListModelMixin,
             	     mixins.RetrieveMixin,
             	     viewsets.GenericViewSet):
             	     # 아까처럼 ReadOnlyModelViewSet으로 한번에 상속할 경우
                     # Update가 커버되지 않아 지금처럼 모두 따로따로 상속하도록 한다.
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer 
    # 아바타 필드를 제외하고는 모든 필드를 update할 수 있다
    permission_classes = [IsAuthenticated]
    
 

6) api/permissions.py 작성하기

from rest_framework import permissions


class IsOwnProfileOrReadOnly(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.user == request.user

7) api/views.py에 방금 작성한 permission import하기

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
# from rest_framework.viewsets import ReadOnlyModelViewSet

from rest_framework import viewsets
from rest_framework import mixins

from profiles.models import Profile
from profiles.api.permissions import IsOwnProfileOrReadOnly
from profiles.api.serializers import ProfileSerializer

class ProfileViewSet(mixins.UpdateModelMixin,
		     mixins.ListModelMixin,
             	     mixins.RetrieveMixin,
             	     viewsets.GenericViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer 
    permission_classes = [IsAuthenticated, IsOwnProfileOrReadOnly] 
    # api.permissions를 통해 임포트한 IsOwnProfileOrReadOnly도 넣어준다
    

여기까지 한 후, 다시
runserver, 바로 127.0.0.1:8000/api/profiles/3 으로 들어간다.
아까와 같은 화면이 보인다. 왜냐하면 현재 admin으로 로그인되어 있기에 id=3의 프로파일을 작성한 resttest 유저가 아니므로 수정 권한을 받지 못하기 때문이다.

하지만 pk를 1로 하여, 즉 admin 유저가 작성한 프로파일로 들어가면 해당 프로파일을 작성한 유저에 해당하므로, update 권한을 부여받은 것을 확인할 수 있다.

7) 이번에는 Profile이 아니라 ProfileStatus 모델에 대한 endpoint를 작성해볼 차례이다.
api/views.py

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewset 
# ProfileStatus에 대한 엔드포인트를 만들기 위해 위 코드를 작성한다

from rest_framework import viewsets
from rest_framework import mixins

from profiles.models import Profile, ProfileStatus # ProfileStatus 임포트
from profiles.api.permissions import IsOwnProfileOrReadOnly
from profiles.api.serializers import ProfileSerializer, ProfileStatusSerializer # ProfileStatusSerializer 임포트

class ProfileViewSet(mixins.UpdateModelMixin,
		     mixins.ListModelMixin,
             	     mixins.RetrieveMixin,
             	     viewsets.GenericViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer 
    permission_classes = [IsAuthenticated, IsOwnProfileOrReadOnly] 


class ProfileStatusViewSet(ModelViewSet):
# ModelViewSet은 mixins.CreateModelMixin, mixins.RetrieveModelMixin, 
# mixins.UpdateModelMixins, mixins.DestroyModelMixins, 
# mixins.ListModelMixin, GenericViewSet()을 모두 상속받는다.
# == create(), retrieve(), update(), partial_update(), destroy(), list() 액션 모두 사용 가능


    queryset = ProfileStatus.objects.all()
    serializer_class = ProfileStatusSerializer 
    permission_classes = [IsAuthenticated, IsOwnProfileOrReadOnly] 

ProfileStatusViewSet에 새로운 permission이 필요하여,
api/permissions.py로 이동

from rest_framework import permissions

class IsOwnProfileOrReadOnly(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.user == request.user


class IsOwnerOrReadOnly(permissions.BasePermission):
# ProfileStatusViewSet에 사용할 Permission은 
# ProfileStatus에 대해 R은 물론이고 U, D가 모두 가능해야하므로 이름을 더욱 강력하게 만든다 
# 그래서 이름이 IsOwnerOrReadOnly 

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.user_profile == request.user.profile

다시
api/views.py로 와서 방금 만든 permission을 임포트해준다.

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewset 

from rest_framework import viewsets
from rest_framework import mixins

from profiles.models import Profile, ProfileStatus 
from profiles.api.permissions import IsOwnProfileOrReadOnly
from profiles.api.serializers import ProfileSerializer, ProfileStatusSerializer 

class ProfileViewSet(mixins.UpdateModelMixin,
		     mixins.ListModelMixin,
             	     mixins.RetrieveMixin,
             	     viewsets.GenericViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer 
    permission_classes = [IsAuthenticated, IsOwnProfileOrReadOnly] 


class ProfileStatusViewSet(ModelViewSet):
    queryset = ProfileStatus.objects.all()
    serializer_class = ProfileStatusSerializer 
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] # 방금 만든 permission으로 바꾸어준다. 

8) 그리고 새로운 ProfileStatus 인스턴스가 생김과 동시에 바로 이 요청을 보낸 해당 유저의 Profile에도 자동으로 변화가 생기기를 원하기 때문에 다음과 같은 메소드를 추가한다.

class ProfileStatusViewSet(ModelViewSet):
    queryset = ProfileStatus.objects.all()
    serializer_class = ProfileStatusSerializer 
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] 
    
    def perform_create(self, serializers):
    	user_profile = self.request.user.profile
        serializer.save(user_profile=user_profile)

9) api/urls.py로 가서 views.py에 새롭게 생긴 클래스의 라우터를 정의하자.

from django.urls import path
from rest_framework.routers import DefaultRouter 
from profiles.api.views import ProfileViewSet, ProfileStatusViewSet # 임포트

router = DefaultRouter() 
router.register(r"profiles", ProfileViewSet) 
router.register(r"status", ProfileStatusViewSet)
# router에 status 엔드포인트 등록, 해당 엔드포인트가 사용할 ViewSet 추가 지정

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

여기까지 한 후, 다시
runserver, 바로 127.0.0.1:8000/api/ 로 들어가면 이제 이런 화면을 볼 수 있다.

status로 라우팅할 수 있는 엔드포인트가 보인다. 해당 엔드포인트로 들어가면

다음처럼 Status를 적을 수 있다.
Hello World! 라고 status를 작성하면 아래처럼 POST 메소드로 보내어지며 status가 작성된다.

그리고 해당 Profile Status들은 작성한 유저가 아니면 U, D 기능은 사용할 수 없다.


위에서 보았듯 ViewSet과 Router를 함께 사용할 경우 매우 막강한 View를 작성할 수 있다.
그리고 만일 ViewSet과 ConcreteView를 합쳐 사용할 경우 가장 이상적인 콤비네이션이 될 수도 있다.

User Profile에서 Avatar는 이미지 필드로 사용할 예정이라 Serializer에서도 따로 분리해서 만들었었다.

따라서 views.py에는 Avatar관련 뷰가 필요하다. 이 뷰를 ConcreteView로 만들어보자.

10) api/views.py

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewset 

from rest_framework import viewsets
from rest_framework import mixins

from profiles.models import Profile, ProfileStatus
from profiles.api.permissions import IsOwnProfileOrReadOnly
from profiles.api.serializers import (ProfileSerializer, 
				     ProfileStatusSerializer, 
                                     ProfileAvatarSerializer)
                                     # Avatar serializer 임포트


# 아래의 뷰를 새롭게 작성해보자.
class AvatarUpdateView(generics.UpdateAPIView):
    serializer_class = ProfileSerializer 
    permission_classes = [IsAuthenticated] 
    # 왜 queryset은 안쓰냐? 하면! 아래 get_object 메소드를 쓰기 때문이다.
    
    def get_object(self):
    	profile_object = self.request.user.profile
        return profile_object
        
	
class ProfileViewSet(mixins.UpdateModelMixin,
		     mixins.ListModelMixin,
             	     mixins.RetrieveMixin,
             	     viewsets.GenericViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer 
    permission_classes = [IsAuthenticated, IsOwnProfileOrReadOnly] 


class ProfileStatusViewSet(ModelViewSet):
    queryset = ProfileStatus.objects.all()
    serializer_class = ProfileStatusSerializer 
    permission_classes = [IsAuthenticated, IsOwnProfileOrReadOnly] 

새롭게 등록된 View가 있으면 urls.py를 수정해야하는 법

api/urls.py

from django.urls import path
from rest_framework.routers import DefaultRouter 
from profiles.api.views import ProfileViewSet, ProfileStatusViewSet, AvatarUpdateView # 임포트

router = DefaultRouter() 
router.register(r"profiles", ProfileViewSet) 
router.register(r"status", ProfileStatusViewSet)

urlpatterns = [
	path("", include(router.urls))
    path("avatar/", AvatarUpdateView.as_view(), name="avatar-update")
    # AvatarUpdateView는 ViewSet과 router를 사용하지 않았으므로 기존 방식으로 path 지정
]

여기까지 한 후, 다시
runserver, 바로 127.0.0.1:8000/api/ 로 들어가서 resttest 계정으로 로그인한다.
이후 127.0.0.1:8000/api/avatar로 접속하면

이런 화면이 나온다. 아바타사진을 등록할 수 있는 버튼이 있다. 사진을 등록한다.

이후
127.0.0.1:8000/api/profiles/에서
resttest의 아바타 사진 url 값을 json으로 확인할 수 있다.

profile
Backend 개발 학습 아카이빙 블로그입니다. (현재는 작성하지 않습니다.)

0개의 댓글