DRF의 제네릭 뷰(Generic views)를 이용해 API 구축하기

mynghn·2022년 10월 27일
1
post-thumbnail

웹 서비스의 내부 API 동작은 대부분 DB 쪽 데이터(쟝고의 경우 모델 레이어)를 다루는 작업을 수반한다.

이런 경우 반복적으로 발생하는 일반적인 패턴이 있고,

Django REST Framework(이하 DRF)에선 이런 일반적인 케이스들에 대해, 클래스 기반 뷰의 형태로 로직을 미리 구현해놓고 제네릭 뷰(Generic views)란 이름으로 제공하고 있다.

여기서 말하는 반복적이고 일반적인 패턴이란
서비스 내 특정 데이터 모델에 대한 CRUD 패턴인데,

따라서 DRF가 미리 구현해놓은 제네릭 뷰를 사용하면
특정 데이터 리소스에 대한 CRUD 액션을 지원하는 웹 API쉽고 효율적으로 구현할 수 있다.

✔️ API의 동작을 결정하는 제네릭 뷰의 네 가지 요소

실제 개발 과정에서 제네릭 뷰를 상속 받아 사용할 땐
구현하는 뷰 클래스에서 네 가지 속성을 정의해주는 것으로 API의 동작을 큰 틀에서 모두 결정할 수 있다.

from rest_framework.generics import GenericAPIView
from rest_framework.authentication import BaseAuthentication
from rest_framework.permissions import BasePermission
from django.db.models.query import QuerySet
from rest_framework.serializers import BaseSerializer

class MyGenericView(GenericAPIView):
	authentication_classes: list[BaseAuthentication]
	permission_classes: list[BasePermission]
	queryset: QuerySet
	serializer_class: BaseSerializer
    ...
  • 요청자를 식별authentication_classes
  • API 요청의 권한을 검증할 permission_classes
  • 작업의 대상이 될 데이터의 범위를 결정할 queryset
  • 요청과 응답의 스펙을 정의하고 뷰 로직의 많은 부분을 담당할 serializer_class

이는 제네릭 뷰로 만든 API가 실제로 동작하는 흐름이 다음과 같기 때문인데,

  1. 들어온 API 요청의 주체를 확인한다.
  2. 그래서 그 요청자에게 작업에 대한 권한이 있는지 확인한다.
  3. 권한이 인증됐으면 작업의 대상이 될 수 있는 데이터를 선별한다.
  4. 선별한 데이터의 범위 안에서 뷰 로직을 적용한 결과를 응답으로 반환한다.

여기서 API의 내부 로직에 해당하는 3, 4번 단계를

  • DRF의 기본 클래스 기반 뷰인 APIView에선 사용자에게 구현을 맡기는 데 반해
  • 제네릭 뷰의 경우는 CRUD 패턴에 대한 구현을 미리 해놓은 것이다.

그리고 그런 제네릭 뷰의 구현 방식에 따라,
serializer_classqueryset의 두 가지 요소가 제네릭 뷰에서는 추가로 필요해진 것이라고 볼 수 있겠다.

authentication_classespermission_classes 제네릭 뷰가 아닌 DRF의 기본 APIView를 사용할 때도 API의 동작을 결정하는 요소가 된다.

🔍 제네릭 뷰가 네 가지 요소를 활용하는 방식

앞서 제네릭 뷰에서 네 가지 요소를 정의하는 방법으로
클래스 속성을 활용하는 정적인 방식을 먼저 소개했지만,

실제 동작 과정에서 이들 요소를 불러오는 역할을 하는 특정 메소드들이 따로 있고,

제네릭 뷰는 이들을 API 요청마다 매번 호출하는 방식으로
실제 활용 시에는 네 가지 요소를 동적으로 처리한다.

*해당 메소드들의 기본 구현이 클래스 속성을 그대로 불러오는 것이기 때문에 클래스 속성을 정의해두는 방식으로 네 가지 요소를 정적으로 선언하는 것이 가능하다.

따라서 관련 메소드들을 오버라이드하면,
API 요청에 따른 네 가지 요소들의 결정 시나리오를 동적으로 프로그래밍하는 것도 가능하다.

1. authentication_classes

  • .get_authenticators(self)

2. permission_classes

  • .get_permissions(self)

3. queryset

  • .get_queryset(self)
  • .filter_queryset(self, queryset)

4. serializer_class

  • .get_serializer_class(self)
  • .get_serializer_context(self)
  • .get_serializer(self, ...)

먼저 authentication_classespermission_classes 속성에 관여하는 메소드는 하나씩밖에 없기에 이들에 대한 모든 동적인 처리 로직은 하나의 메소드 안에 작성하면 돼서 간단하다.

이에 반해 querysetserializer_class는 하나 이상의 메소드를 거치는데,
이들 각각의 역할과 오버라이드 시나리오는 아래에서 보다 자세하게 부연할 예정이다.

어쨌든 동적인 처리 로직은 프로젝트마다 요구 사항에 맞게 구현하면 되는 것이므로,
이 글에서는 기본적으로 클래스 속성을 통한 정적인 선언을 기준으로 설명하겠다.

authentication_classes: 유저 인증

유저 인증 백엔드를 지정하는 작업은 제네릭 뷰가 아닌 기본 APIView를 사용할 때도 똑같이 적용된다.

DRF의 클래스 기반 뷰에서 authentication_classes 목록을 정의하면 해당 API에서 사용할 유저 인증 백엔드들을 지정할 수 있다.

from rest_framework.views import APIView
from rest_framework_simplejwt.authentication import JWTAuthentication

class MyAPIView(APIView):
	authentication_classes = [JWTAuthentication]
    ...

인증 백엔드로 받을 수 있는 객체는 DRF의 유저 인증 인터페이스를 갖춘 클래스면 되는데,

위의 예시에서 등장한 Simple JWT 라이브러리의 JWTAuthentication 클래스 역시,
JWT 기반 인증을 처리하는 커스텀 백엔드를 DRF의 인터페이스를 따르는 형태로 구현한 것이다.

DRF의 유저 인증 인터페이스를 갖추는 법은 간단한데,

먼저 DRF의 BasicAuthentication 클래스를 상속 받은 뒤, 다음과 같이 .authenticate(self, request) 메소드를 오버라이드하면 된다.

  • 인증 성공 시,
    인증된 유저 인스턴스와 인증 정보를 담은 (user, auth) 튜플을 리턴
  • 인증 실패 시,
    • None 리턴
    • 혹은 DRF의 AuthenticationFailed 예외 발생

그리고 제네릭 뷰는 이런 인터페이스를 갖춘 인증 백엔드 목록에서
성공하는 게 나올 때까지 차례로 요청에 대한 인증을 시도한다.

사용할 인증 백엔드를 개별 API 마다가 아니라 프로젝트 전체 레벨에서 설정하는 것도 가능하다.

DRF의 APIView 구현 상, 프로젝트 설정 모듈의 DRF 설정값 딕셔너리에서 DEFAULT_AUTHENTICATION_CLASSES 값을 불러와 authentication_classes 속성 기본값으로 지정하기 때문에,

프로젝트 설정값이 있다면 API를 개설할 때마다 인증 백엔드를 지정하지 않고 이를 그대로 불러와 사용할 수 있다.

# rest_framework/views.py
from django.views.generic import View
from rest_framework.settings import api_settings

class APIView(View):
	...
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
# my_project/settings.py
REST_FRAMEWORK = {
    ...,
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ),
}

permission_classes: 권한 검증

권한 검증 백엔드를 지정하는 작업은 제네릭 뷰가 아닌 기본 APIView를 사용할 때도 똑같이 적용된다.

DRF의 클래스 기반 뷰에서 permission_classes 목록을 정의하면 해당 API에서 사용할 권한 검증 백엔드들을 지정할 수 있다.

from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated

class MyAPIView(APIView):
	permission_classes = [IsAuthenticated]
    ...

유저 인증 백엔드의 경우와 비슷하게
DRF의 인터페이스를 갖춘 클래스는 권한 검증 백엔드로 활용할 수 있다.

DRF의 권한 검증 인터페이스를 갖추는 법을 간단히 요약하면,

  • DRF의 BasePermission 클래스를 상속 받은 뒤,

  • 권한 검증 통과 여부를 리턴하는

    • .has_permission(self, request, view)
    • .has_object_permission(self, request, view, obj)

    메소드 중 최소 하나를 필요에 따라 오버라이드하면 된다.

권한 검증은 유저 인증 결과를 가지고 진행해야 하기 때문에,
DRF는 앞서 일어난 유저 인증의 결과를 인자로 들어오는 request 객체의 request.userrequest.auth 안에 저장해서 넘긴다.
권한 검증 단계에서는 이를 보고 내부 로직을 진행한다.

전체 모델에 대한 권한이 아니라 그 중에서도 특정 인스턴스(들)에 대한 권한을 기준으로 검증할 수도 있는데,
.has_object_permission() 메소드 안에서 이런 오브젝트 레벨의 권한 검증을 진행할 수 있다. (자세한 내용은 공식 문서 참고)

그리고 제네릭 뷰에선 이런 인터페이스를 갖춘 백엔드 목록에 대해
권한 검증을 모두 통과해야 API 요청에 대한 응답이 허용된다.

  • 앞서 살펴본 유저 인증 백엔드 목록에는 OR 조건이
  • 권한 검증 백엔드 목록에는 AND 조건이 적용된다고 볼 수 있다.

역시 DEFAULT_PERMISSION_CLASSES 설정값을 활용하면,
사용할 권한 검증 백엔드를 프로젝트 전체 레벨에서 설정할 수 있다.

# rest_framework/views.py
from django.views.generic import View
from rest_framework.settings import api_settings

class APIView(View):
	...
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
# settings.py
REST_FRAMEWORK = {
    ...,
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly"
    ]
}

queryset: 리소스 정의

제네릭 뷰를 활용해 구축하려는 API의 기능이 오로지 CREATE 액션 하나인 경우가 아니라면,
(자세한 이유는 아래에)

queryset 속성을 통해 해당 API에서 취급할 데이터의 바운더리를 정의하는 과정이 필요하다.

from rest_framework.generics import GenericAPIView
from .models import MyModel

class MyGenericView(GenericAPIView):
	queryset = MyModel.objects.all()
    ...

기본적으로 구축할 API의 입장에서 URI에 대응하는 데이터 리소스를 정의해주는 작업이라고 보면 된다.

  • api/users/ 👉 User.objects.all()
  • api/movies/ 👉 Movie.objects.all()

그리고 예외적으로 특정한 조건을 만족하는 리소스들이 대상이라면 다음과 같이 할 수도 있다.
ex) User.objects.filter(is_staff=False), Movie.objects.filter(genre="스릴러")

♻️ 동적으로 쿼리셋 제한하기

만약 요청에 따라 쿼리셋을 동적으로 결정해야 한다면,
위에서 언급한 관련 메소드들이 실행되는 흐름을 보고 역할에 맞는 메소드를 찾아 오버라이드해주면 되겠다.

>>> queryset = self.filter_queryset(self.get_queryset())
  1. 먼저 .get_queryset(self)이 불리고
  2. 그 결과로 반환된 쿼리셋을 .filter_queryset(self, queryset)에 넘겨 필터링한다.

따라서,

  • 만약 모델의 종류 자체를 동적으로 변경해야 하는 시나리오라면 가장 외부의 .get_queryset()에서 이를 처리해주면 되고,
  • 같은 모델 안에서 요청에 따라 인스턴스 레벨의 필터링이 필요한 거라면 .filter_queryset()으로도 충분하겠다.

여기서 또 필터링은 filter_backends라는 제네릭 뷰의 클래스 속성을 활용하면 별도의 오버라이드 없이도 구현이 가능한데

from rest_framework.generics import GenericAPIView
from rest_framework.filters import BaseFilterBackend

class MyGenericView(GenericAPIView):
    filter_backends: list[BaseFilterBackend]
    ...

제네릭 뷰의 .filter_queryset() 기본 구현이 이 filter_backends 목록을 순회하면서 차례로 쿼리셋을 필터링하는 것이기 때문에

# rest_framework/generics.py
class GenericAPIView(APIView):
	...
	def filter_queryset(self, queryset):
        ...
        for backend in list(self.filter_backends):
            queryset = backend().filter_queryset(self.request, queryset, self)
        return queryset

DRF의 인터페이스를 갖춘 필터링 백엔드 목록을 정의해두면 이들 모두를 거친 최종 결과를 쿼리셋으로 활용할 수 있다.

DRF의 쿼리셋 필터링 인터페이스에 대한 내용은 공식 문서 참고

필터링 백엔드 목록 역시, 앞선 경우와 비슷하게 DEFAULT_FILTER_BACKENDS 설정값을 통해 프로젝트 레벨에서 정의가 가능하다.

serializer_class: 데이터 조작

마지막으로 제네릭 뷰에서 활용할 수 있는 시리얼라이저를 미리 구현해 넘겨주면,
해당 API의 내부 로직에 필요한 많은 부분을 맡아서 처리해준다.

from rest_framework.generics import GenericAPIView
from .serializers import MySerializer

class MyGenericView(GenericAPIView):
	serializer_class = MySerializer
    ...

제네릭 뷰에서 시리얼라이저가 담당하는 작업은 다음과 같다.

  • API 요청/응답 스펙 결정
    : 시리얼라이저에서 필드를 정의하는 것으로 요청으로 받고 응답으로 돌려줄 데이터 스펙이 결정된다.
  • API 요청 유효성 검증
    : 응답 이전에 .is_valid() 메소드를 거쳐, 들어온 요청 payload의 데이터 유효성을 검증한다.
  • 모델 인스턴스 생성/수정 후 DB에 반영
    : 유효성 검증 이후 시리얼라이저의 .save() 메소드를 통해 모델 인스턴스로의 역직렬화와 DB 반영까지 수행한다.
  • API 응답 직렬화
    : 작업 대상이었던 모델 인스턴스(들)을 .data 프로퍼티에 접근해 직렬화한 결과를 응답으로 돌려준다.
  • CREATE 액션의 경우 데이터 리소스 정의까지
    : CREATE 액션의 경우엔 별도의 쿼리셋 없이 시리얼라이저의 동작으로 취급하는 데이터 리소스가 정의된다.

들어온 요청의 CRUD 유형에 따라,
위의 작업 중 내부 프로세스에서 필요한 (거의 대부분의) 작업을 시리얼라이저가 처리한다.

결국 제네릭 뷰는 API 작업의 전체 프로세스를 관리하는 것이고, 실질적인 데이터 조작이 필요해지는 순간에는 시리얼라이저를 부른다고 볼 수 있다.

그리고 일반적으로 제네릭 뷰를 통해 처리하고자 하는 데이터는 쟝고의 모델 레이어일 텐데,
그래서 자연스럽게 제네릭 뷰에 넘기는 시리얼라이저는 대부분 ModelSerializer가 된다.
👉 그렇지 않으면 모델 레이어 쪽을 조작하는 작업 인터페이스를 모두 직접 구현해야 함

♻️ 동적으로 시리얼라이저 구성하기

이제 요청에 따라 시리얼라이저를 다르게 구성해야 하는 경우를 위해,
관련 메소드들의 호출 흐름을 통해 제네릭 뷰가 매 요청에 사용할 시리얼라이저를 구성하는 로직을 살펴보자.

# rest_framework/generics.py
class GenericAPIView(APIView):
	...
	def get_serializer(self, *args, **kwargs):
        ...
        serializer_class = self.get_serializer_class()
        kwargs.setdefault("context", self.get_serializer_context())
        return serializer_class(*args, **kwargs)
  1. 먼저 가장 바깥에서 .get_serializer(self, *args, **kwars)가 불린다.
  2. 그 안에서 .get_serializer_class(self)를 통해 사용할 시리얼라이저 클래스를 가져온다.
  3. 그리고 .get_serializer_context(self)를 통해 시리얼라이저에게 넘겨줄 뷰 레이어 레벨의 정보가 담긴 컨텍스트를 만든다.
  4. 마지막으로 처음에 넘어온 인자와 앞서 생성한 컨텍스트를 모두 생성자로 넘겨 시리얼라이저 객체 생성 후 리턴.

따라서 메소드별 역할에 따라 다음과 같은 오버라이드 시나리오가 가능하겠다.

  • 시리얼라이저의 생성자 호출 순간에 관여해야 하면, .get_serializer() 단에서의 커스텀이 필요할 것이고,
  • 시리얼라이저의 종류를 결정해야 한다면, .get_serializer_class()를 오버라이드 하는 것으로 충분.
  • 예외적으로, 뷰 레이어의 정보를 추가로 시리얼라이저 객체에 주입해야 한다면 추가 로직을 .get_serializer_context() 안에서 구현하면 된다.

그리고 기본적으로 DRF의 시리얼라이저는 생성자로 넘기는 인자에 따라 직렬화와 역직렬화로 이후 시나리오가 달라지는데

  • Serializer(data=data) 👉 역직렬화
  • Serializer(instance) 👉 직렬화

따라서 제네릭 뷰는 API 요청에 따라 상황에 맞게 인자를 구성해서 .get_serializer() 메소드에 넘기도록 구현이 되어 있다.
👉 그러면 .get_serializer()가 안에서 인자를 생성자로 넘겨 시리얼라이저 객체 생성

# rest_framework/mixins.py
class CreateModelMixin:
    ...
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        ...

class ListModelMixin:
    ...
    def list(self, request, *args, **kwargs):
        ...
        serializer = self.get_serializer(queryset, many=True)
        ...

class RetrieveModelMixin:
    ...
    def retrieve(self, request, *args, **kwargs):
        ...
        serializer = self.get_serializer(instance)
        ...

class UpdateModelMixin:
    ...
    def update(self, request, *args, **kwargs):
        ...
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        ...

💡 요약

  • DRF의 제네릭 뷰 API(코드)를 사용하면 데이터에 대한 CRUD 작업을 수행하는 웹 API를 쉽게 개발할 수 있다.

  • 제네릭 뷰로 만드는 일반적인 API의 동작은 네 가지 요소를 통해 결정된다.

    1. 유저 인증: authentication_classes
    2. 권한 검증: permission_classes
    3. 리소스 정의: queryset
    4. 데이터 조작: serializer_class
  • 위의 네 가지 요소에 대한 정책을 (동적으로든 정적으로든) 하나씩 결정해주는 과정으로, 제네릭 뷰를 활용한 API 구축 작업을 정의할 수 있다.

0개의 댓글