본문 바로가기
  • 실행력이 모든걸 결정한다
Django Rest Framework

[DRF] DRF 뷰의 발전 과정

by 김코더 김주역 2023. 2. 23.
반응형

1. DRF의 5가지 주요 기능

- DRF 뷰의 발전 과정을 살펴보기 전에 알아두면 좋을 것 같아서 추가한 내용이다.

 

1) list

- 데이터 목록을 가져오는 기능

- 예) GET /snacks/

 

2) create

- 데이터 하나를 추가하는 기능

- 예) POST /snacks/

 

3) retrieve

- 데이터 하나를 가져오는 기능

- 예) GET /snack/1/

 

4) update

- 데이터 하나를 수정하는 기능

- 예) PUT /snack/1/

 

5) destroy

- 데이터 하나를 삭제하는 기능

- 예) DELETE /snack/1/

 

 

 

2. DRF 믹스인

- DRF는 메소드마다 반복적이면서 공통적으로 작성해야 하는 코드들을 줄이기 위한 믹스인 클래스들을 제공한다. 예를 들어, 모델 데이터를 가져오는 코드나 시리얼라이저 코드, 또는 각 HTTP 메소드별로 처리해야하는 코드 등이 해당된다.

 

1) 뷰에 DRF 믹스인 적용

이전 포스팅에서 작성했던 순수 DRF 클래스형 뷰에 믹스인을 적용해보자.

 

믹스인 적용 전(순수 DRF 클래스형 뷰)

from rest_framework import viewsets, permissions, generics, status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.decorators import api_view
from rest_framework.generics import get_object_or_404

from .models import Snack
from .serializers import SnackSerializer


class SnacksAPI(APIView):
    def get(self, request):
        snacks = Snack.objects.all()
        serializer = SnackSerializer(snacks, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
    def post(self, request):
        serializer = SnackSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class SnackAPI(APIView):
    def get(self, request, pk):
        snack = get_object_or_404(Snack, pk=pk)
        serializer = SnackSerializer(snack)
        return Response(serializer.data, status=status.HTTP_200_OK)

 

믹스인 적용 후

from rest_framework import generics
from rest_framework import mixins

class SnacksAPI(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView):
    queryset = Snack.objects.all()
    serializer_class = SnackSerializer

    def get(self, request, *args, **kwargs): # mixins.ListModelMixin과 연결
        return self.list(request, *args, **kwargs)
    def post(self, request, *args, **kwargs): # mixins.CreateModelMixin과 연결
        return self.create(request, *args, **kwargs)


class SnackAPI(mixins.RetrieveModelMixin, generics.GenericAPIView):
    queryset = Snack.objects.all()
    serializer_class = SnackSerializer
    # lookup_field = 'pkpk' # 기본 모델 pk를 사용하고 있지 않을 경우에 별도로 추가한 기본키 필드를 설정
    
    def get(self, request, *args, **kwargs): # mixins.RetrieveModelMixin과 연결
        return self.retrieve(request, *args, **kwargs)

- 클래스 레벨에서 쿼리셋과 시리얼라이저를 등록하고, 각 메소드에 해당하는 믹스인 클래스들을 연결함으로써 메소드마다 반복적으로 나타나는 코드를 대폭 줄였다. 메소드의 이름은 [1. DRF의 5가지 주요 기능]에서 살펴본 키워드와 동일하다.

- GenericAPIView는 공통적으로 상속받아야 한다.

 

이왕 코드가 짧아져서 편해졌으니, update 기능과 destroy 기능도 추가해보았다.

class SnackAPI(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView):
    ...
    
    def put(self, request, *args, **kwargs): # mixins.UpdateModelMixin과 연결
        return self.update(request, *args, **kwargs)
    def delete(self, request, *args, **kwargs): # mixins.DestroyModelMixin과 연결
        return self.destroy(request, *args, **kwargs)

 

URLConf 설정은 다음과 같다. as_view()로 호출해주기만 하면, HTTP 메소드에 맞는 매핑 함수로 알아서 매핑시켜준다.

# snack/urls.py

app_name='snack'
urlpatterns = [
    # /snack/mixin/snacks
    path('mixin/snacks', SnacksAPI.as_view()),
    # /snack/mixin/snack
    path('mixin/snack/<int:pk>/', SnackAPI.as_view()),
]

 

이제 본격적으로 각 기능들을 테스트해보도록 하자.  먼저 list 기능이다.

 

그 다음은 create 기능이다.

스크롤을 아래로 내려보면 HTML 폼이 나타날텐데, 이 폼은 믹스인이 제공해준 것이다. 이 곳에 원하는 데이터를 입력하고 POST를 누르면 된다.

정상적으로 POST 요청에 성공했다면, 방금 추가한 데이터 하나만이 나타난다. 조금 더 내려보면 수정폼도 보일 것이다.

 

그 다음은 retrieve 기능과 update 기능이다. 바로 위에 첨부한 POST 요청 성공 화면과 동일한 화면이 나올 것이다. 해당 모델 객체의 데이터가 폼에 채워져 있으며, 수정이 필요하다면 내용을 바꾸고 PUT 버튼을 누르면 된다.

 

마지막으로 destroy 기능이다. DELETE를 누르기만 하면 성공적으로 삭제된다.

성공적으로 삭제되었다면 다음과 같이 데이터가 사라져 있을 것이다.

 

 

2) perform_[action] 메소드

- create, update, destroy를 담당하는 믹스인에는 perform_[action] 메소드가 있다.

- 단순히 시리얼라이저의 save() 또는 delete() 메소드를 호출하는 것이 전부이지만, 수행 전에 추가적으로 다른 작업을 해야 하는 경우에 이 perform_[action] 메소드를 오버라이딩해서 사용하라는 의도로 만들어진 것이다.

 

 

 

3. DRF generics

- 뷰 클래스에 너무 많은 믹스인들을 상속받는 것이 불편하다고 생각한 사람들이 있었기 때문에, 믹스인들을 미리 조합해놓은 클래스가 생겨났다. generics에는 다음과 같이 총 9개의 믹스인 조합 클래스가 있다.

  • generics.ListAPIView : 전체 목록 조회
  • generics.CreateAPIView : 데이터 하나를 생성
  • generics.RetrieveAPIView : 데이터 하나를 조회
  • generics.UpdateAPIView : 데이터 하나를 수정
  • generics.DestroyAPIView : 데이터 하나를 삭제
  • generics.ListCreateAPIView : 전체 목록 조회 + 데이터 하나를 생성
  • generics.RetrieveUpdateAPIView : 데이터 하나를 조회, 수정
  • generics.RetrieveDestroyAPIView : 데이터 하나를 조회, 삭제
  • generics.RetrieveUpdateDestroyAPIView : 데이터 하나를 조회, 수정, 삭제

 

이제 DRF generics를 사용하여 [2. DRF 믹스인]에서 사용했던 코드를 줄여보자.

from rest_framework import generics


class SnacksAPI(generics.ListCreateAPIView):
    queryset = Snack.objects.all()
    serializer_class = SnackSerializer


class SnackAPI(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snack.objects.all()
    serializer_class = SnackSerializer

기가 막힌다. 메소드도 필요가 없어졌다. 기존에 있던 메소드들은 generics의 각 API View들이 가지고 있기 때문이다.

그런데 위 코드에 있는 patch() 메소드는 무엇일까? 내부적으로 partial_update()를 호출하고 있는 모습이다.

patch는 put와 동일하게 데이터 하나의 수정을 담당한다는 공통점이 있지만, 수정하지 않을 속성까지도 같이 전송하는지에 대한 차이가 존재한다.

put은 일부 속성만 수정할 것이라도 데이터의 모든 속성을 전송하고, patch는 수정할 속성만 전송한다는 차이가 있다.

예를 들어, {id, name, age} 속성을 가지는 모델의 객체에서 age 속성만 수정하는 경우를 생각해보자. 이 때 PUT 요청시 {"id": 5, "name": "kim", "age": 26}을 전송하면 되고, PATCH 요청시 {"age": 26}을 전송하면 된다.

 

 

 

4. DRF Viewset

- 지금까지 작업했던 것은 하나의 클래스가 하나의 URL을 담당하고, 클래스의 각 메소드는 특정 HTTP 메소드에 대한 요청을 처리해주었다. 그러다 보니 queryset과 serializer_class 부분이 반복적으로 작성되었는데, Viewset은 이러한 반복 마저 없애준다.

 

1) ModelViewSet

- viewsets.ModelViewSet을 상속받는 것만으로도 list, create, retrieve, update, destroy 5가지 기능을 모두 가지게 된다. 개발자는 queryset과 serializer_class만 지정해주면 된다.

from rest_framework import viewsets


class SnackViewSet(viewsets.ModelViewSet):
    queryset = Snack.objects.all()
    serializer_class = SnackSerializer

viewsets.ModelViewSet의 내부를 보면, 이 5가지 기능을 각각 담당하는 믹스인 클래스들을 모두 가지고 있는 모습이다.

 

 

2) 라우터

- Viewset을 URL과 연결하기 위한 수단이다.

- 라우터 객체를 만들고 아까 만든 Viewset을 라우터 객체에 등록해주고 urlpatterns에 라우터 객체의 urls를 추가해주면 된다.

# djangoProject/urls.py

from rest_framework import routers
from snack.views import SnackViewSet

urlpatterns = []
...

router = routers.SimpleRouter()
router.register('snack', SnackViewSet) # 첫 번째 인자는 URL prefix를 의미한다.

urlpatterns += router.urls

 

필자는 프로젝트의 urls.py에 라우터를 만들었으며, URL 매핑은 다음과 같이 이루어진다.

[list route]

  • function : list, create
  • url : /snack/ (형식 : /{prefix})
  • url name : snack-list (형식 : {모델 소문자 이름}-list)

 

[detail route]

  • function : retrieve, update, partial_update, destroy
  • url : /snack/1/ (형식 : /{prefix}/{pk}/)
  • url name : snack-detail (형식 : {모델 소문자 이름}-detail)

 

- Viewset과 Router를 통해 하나의 클래스로 하나의 모델에 대한 내용을 전부 작성할 수 있었고, URL을 일일이 지정하지 않아도 일정한 규칙의 URL을 만들 수 있었다. 뒤로 갈수록 코드의 양은 줄었지만 그만큼 자유도는 낮아진다는 단점 또한 존재한다. 그러니 어떤 방법이 정답이라고 확정 짓는 것보다는, 자신의 프로젝트에 가장 적절한 방법을 찾는 것이 좋다.

 

 

3) ViewSet에 매핑 함수 추가하기 : @action 데코레이터

- Viewset에서 제공하는 디폴트 매핑 외에도 추가 매핑이 필요할 경우가 있다. 매핑 함수는 Viewset의 멤버 함수로 구현한 후 @action 데코레이터를 붙여주면 된다.

def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
    ...

(1) methods 속성 : HTTP 메소드 이름 리스트를 지정하며, 디폴트는 ['get'] 이다.

(2) detail 속성 : pk값의 지정 여부를 Boolean 값으로 지정한다. detail 값에 따라 URL과 URL 이름도 다음과 같이 바뀌게 된다.

[detail=True의 경우]

  • url : /{prefix}/{pk}/{매핑 함수 이름}/
  • url name : {모델 소문자 이름}-{매핑 함수 이름}

[detail=False의 경우]

  • url : /{prefix}/{매핑 함수 이름}/
  • url name : {모델 소문자 이름}-{매핑 함수 이름}

※ url name의 {매핑 함수 이름}의 _(밑줄)은 -(하이픈)으로 교체된다.

(3) url_path 속성 : 매핑 URL 패턴을 별도로 지정한다. URL 패턴에 파라미터를 부여하는 경우에는 regex 표현식을 사용해야 한다.

@action(detail=False, url_path=r"monthly_list/(?P<year>\w+)/(?P<month>\w+)")

(4) url_name 속성 : 매핑 URL의 이름을 별도로 지정

 

[@action 매핑 함수 사용 예시]

class SnackViewSet(viewsets.ModelViewSet):
    queryset = Snack.objects.all()
    serializer_class = SnackSerializer

    # /snack/accepted_snack_list/
    @action(detail=False)
    def accepted_snack_list(self, request):
        qs = self.queryset.filter(is_accepted=True)
        serializer = self.get_serializer(qs, many=True)
        return Response(serializer.data)

 

참고로, DRF 공식 문서에 의하면 @action을 통해 permission_classes, serializer_class, filter_backends같은 viewset-level 설정도 오버라이딩할 수 있다고 한다.

@action(detail=True, methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
   ...

 

 

4) 라우터를 사용하지 않고 Viewset 설정하기

- as_view()의 actions 인자에 HTTP 메소드와 뷰셋 메소드의 매핑 정보를 직접 지정해준다.

from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers

snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
    'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
    'get': 'list'
})
user_detail = UserViewSet.as_view({
    'get': 'retrieve'
})

그리고 뷰셋들을 명시적으로 URL에 매핑해주면 된다.

urlpatterns = format_suffix_patterns([
    path('', api_root),
    path('snippets/', snippet_list, name='snippet-list'),
    path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
    path('users/', user_list, name='user-list'),
    path('users/<int:pk>/', user_detail, name='user-detail')
])

 

 

Viewset과 Router에 대해 더 알고 싶다면, 아래 공식 문서를 참고하자.

https://www.django-rest-framework.org/api-guide/viewsets/

 

Viewsets - Django REST framework

viewsets.py After routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. — Ruby on Rails Documentation Django REST framework allows you to combine

www.django-rest-framework.org

https://www.django-rest-framework.org/api-guide/routers/

 

Routers - Django REST framework

 

www.django-rest-framework.org

 

반응형

댓글