왜 DRF를 사용해야할까?

Django REST Framework(DRF)는 RESTful API 개발을 위해 django 위에 추가된 라이브러리로 RESTful API 개발을 위해 필요한 공수를 줄여준다. API를 개발할 때 필요한 authentication, permission, throttling 등을 쉽게 구현할 수 있도록 해주는데 아래에 DRF에서 정의하는 settings만 봐도 API 개발에 필요한 것들을 쉽게 추가할 수 있음을 알 수 있다.

DEFAULTS = {
    # Base API policies
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
    'DEFAULT_PARSER_CLASSES': [
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser'
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication'
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'DEFAULT_THROTTLE_CLASSES': [],
    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
    'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
    'DEFAULT_VERSIONING_CLASS': None,

    # Generic view behavior
    'DEFAULT_PAGINATION_CLASS': None,
    'DEFAULT_FILTER_BACKENDS': [],

    # Schema
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema',

    # Throttling
    'DEFAULT_THROTTLE_RATES': {
        'user': None,
        'anon': None,
    },
    'NUM_PROXIES': None,

    # Pagination
    'PAGE_SIZE': None,

    # Filtering
    'SEARCH_PARAM': 'search',
    'ORDERING_PARAM': 'ordering',

    # Versioning
    'DEFAULT_VERSION': None,
    'ALLOWED_VERSIONS': None,
    'VERSION_PARAM': 'version',

    # Authentication
    'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
    'UNAUTHENTICATED_TOKEN': None,

    # View configuration
    'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
    'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',

    # Exception handling
    'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
    'NON_FIELD_ERRORS_KEY': 'non_field_errors',

    # Testing
    'TEST_REQUEST_RENDERER_CLASSES': [
        'rest_framework.renderers.MultiPartRenderer',
        'rest_framework.renderers.JSONRenderer'
    ],
    'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',

    # Hyperlink settings
    'URL_FORMAT_OVERRIDE': 'format',
    'FORMAT_SUFFIX_KWARG': 'format',
    'URL_FIELD_NAME': 'url',

    # Input and output formats
    'DATE_FORMAT': ISO_8601,
    'DATE_INPUT_FORMATS': [ISO_8601],

    'DATETIME_FORMAT': ISO_8601,
    'DATETIME_INPUT_FORMATS': [ISO_8601],

    'TIME_FORMAT': ISO_8601,
    'TIME_INPUT_FORMATS': [ISO_8601],

    # Encoding
    'UNICODE_JSON': True,
    'COMPACT_JSON': True,
    'STRICT_JSON': True,
    'COERCE_DECIMAL_TO_STRING': True,
    'UPLOADED_FILES_USE_URL': True,

    # Browsable API
    'HTML_SELECT_CUTOFF': 1000,
    'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...",

    # Schemas
    'SCHEMA_COERCE_PATH_PK': True,
    'SCHEMA_COERCE_METHOD_NAMES': {
        'retrieve': 'read',
        'destroy': 'delete'
    },
}

Serializer 클래스로 DB 인스턴스를 python native 데이터 타입으로 변환할 수 있고 그 반대의 과정도 쉽게 구현할 수 있다. 그리고 다양한 추상화 단계에서 API를 개발할 수 있도록 돕는데, 뷰를 예를들면 APIView, GenericAPIView, GenericViewSet 으로 갈수록 추상화 단계가 높아지고 customization의 정도에 따라 필요한 클래스를 상속받아 API를 구현할 수 있다. 특히 viewset과 router를 통해 코드 몇줄로 CRUD를 구현할 수 있어 코드의 중복을 줄일 수 있는데 실제로 아래 예시는 DRF 공식문서 튜토리얼 > Quickstart 의 코드로 viewset과 router 의 조합으로 API를 작성했다. 먼저 serializer를 구현하여 Viewset의 serializer_class에 할당하고, DefaultRouter에 viewset을 등록하여 urlconf를 생성할 수 있다. 그리고 Django에서 기본적으로 제공하는 HttpRequest, HttpResponse 클래스를 확장하여 API 요청을 처리하는데 필요한 기능을 추가로 제공한 Request, Response 클래스를 사용하는데 실제로 Request 클래스를 초기화할 때 parsers, authenticators, negotiator 인자 등을 추가로 받을 수 있다

# Serializers
from django.contrib.auth.models import GroupUser
from rest_framework import serializers


class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ['url', 'username', 'email', 'groups']


# Views
from django.contrib.auth.models import User
from rest_framework import permissions, viewsets


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]


# Urls
from django.urls import include, path
from rest_framework import routers

router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = [
    path('', include(router.urls)),
]

그리고 DRF의 View class hierarchy는 다음과 같으니 참고하자.

View (django.views.generic.base)
    APIView(View) (rest_framework.views)
        APIRootView(views.APIView) (rest_framework.routers)
        GenericAPIView(views.APIView) (rest_framework.generics)
            CreateAPIView(mixins.CreateModelMixin, GenericAPIView) (rest_framework.generics)
            DestroyAPIView(mixins.DestroyModelMixin, GenericAPIView) (rest_framework.generics)
            GenericViewSet(ViewSetMixin, generics.GenericAPIView) (rest_framework.viewsets)
                ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet) (rest_framework.viewsets)
                    GroupViewSet(viewsets.ModelViewSet) (tutorial.quickstart.views)
                    UserViewSet(viewsets.ModelViewSet) (tutorial.quickstart.views)
                ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet) (rest_framework.viewsets)
            ListAPIView(mixins.ListModelMixin, GenericAPIView) (rest_framework.generics)
            ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView) (rest_framework.generics)
            RetrieveAPIView(mixins.RetrieveModelMixin, GenericAPIView) (rest_framework.generics)
            RetrieveDestroyAPIView(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, GenericAPIView) (rest_framework.generics)
            RetrieveUpdateAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, GenericAPIView) (rest_framework.generics)
            RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, GenericAPIView) (rest_framework.generics)
            UpdateAPIView(mixins.UpdateModelMixin, GenericAPIView) (rest_framework.generics)
        Index(views.APIView) (tutorial.quickstart.views)
        ObtainAuthToken(APIView) (rest_framework.authtoken.views)
        SchemaView(APIView) (rest_framework.schemas.views)
        ViewSet(ViewSetMixin, views.APIView) (rest_framework.viewsets)

DRF가 장점도 분명히 있지만 단점도 존재한다. 일단 fastapi나 django-ninja에 비해 코드 가독성이 그닥 좋지 않으며, 문서화의 불편함도 있다. API 작업을 하고 postman 문서을 함께 수정하는 경험은 그닥 좋지 않았다. 물론 drf-yasg 라이브러리로 django에 정의된 serializer를 읽어서 swagger 문서를 자동으로 생성해줄 수 있지만, drf의 모든 view에 대해 serializer 작성을 강제하고 있지 않기도 하고, serializer에 정의되지 않은 커스텀한 response 정보를 문서로 보여줄 때는 코드에 response 응답 정보를 추가로 작성해야했다. 반면에 django-ninja 같은 경우는 코드 자체에서 request, response 스키마를 강제하고 있고, 그 스키마를 바탕으로 문서를 자동으로 생성해주기 때문에, 문서 작업에 들어가는 노력을 현저히 줄일 수 있다. 뿐만 아니라 Serializer 클래스는 속도가 매우 느리다. 이 블로그 를 보면 UserModelSerializer, UserReadOnlyModelSerializer, UserSerializer, UserReadOnlySerializer 에 따라 성능이 10배 이상 차이나는 결과를 보여주며 해당 블로그에서 소개한 python serialization benchmark 에서도 DRF가 가장 느린 것도 볼 수 있다.

DRF serializer 뜯어보기

Serializer를 사용하여 쿼리셋 또는 모델 인스턴스를 python 데이터 타입으로 변환하고, 이를 json, xml과 같은 다른 content type으로 쉽게 렌더링 할 수 있다. 이 과정을 serialization, 그 반대 과정을 deserialization 이라고 한다. 그리고 꽤나 자주 쓰는 Serializer, ListSerializer, ModelSerializer는 모두 BaseSerializer 클래스를 상속한다. 자세한 serializer 클래스들의 hierarchy는 아래를 참고하자. BaseSerializer 클래스를 초기화할 때 instance 또는 data를 입력으로 받는데 instance를 입력으로 받을 땐 serialization 과정이, 반대로 data를 입력으로 받을 땐 deserialization 과정이 진행된다.

Field (rest_framework.fields)
    BaseSerializer(Field) (rest_framework.serializers)
        Serializer(BaseSerializer, metaclass=SerializerMetaclass) (rest_framework.serializers)
            CommentSerializer(serializers.Serializer) (testapps.views)
            ModelSerializer(Serializer) (rest_framework.serializers)
                NestedSerializer(ModelSerializer) (rest_framework.serializers)
                HyperlinkedModelSerializer(ModelSerializer) (rest_framework.serializers)
                    UserSerializer(serializers.HyperlinkedModelSerializer) (testapps.views)
                    GroupSerializer(serializers.HyperlinkedModelSerializer) (testapps.views)
                    NestedSerializer(HyperlinkedModelSerializer) (rest_framework.serializers)
                    UserSerializer(serializers.HyperlinkedModelSerializer) (tutorial.quickstart.serializers)
                    GroupSerializer(serializers.HyperlinkedModelSerializer) (tutorial.quickstart.serializers)
            AuthTokenSerializer(serializers.Serializer) (rest_framework.authtoken.serializers)
        ListSerializer(BaseSerializer) (rest_framework.serializers)

먼저 serialization 과정을 살펴보자. BaseSerializer 의 초기화 인자로 instance를 받는다. 그리고 바로 .data를 호출하여 python dictionary로 가져올 수 있다. .data를 호출하면 내부적으로 to_representation메소드를 호출한다. to_representation 메소드는 기본적으로 입력된 instance의 field를 돌면서 python dictionary 형태로 변환해준다. 물론 to_representation 을 override 하여 custom 하게 데이터 형태를 바꿔 리턴해줄 수도 있다.

반대로 deserialization 과정을 살펴보자. BaseSerializer의 초기화 인자로 data 인자를 받는다. 하지만 .data 로 바로 python dictionary를 가져올 수 없고 is_valid 메소드를 먼저 호출해야한다. 여기서 run_validationto_internal_value 메소드가 순차적으로 실행되어 serializer의 필드명과 입력된 data의 필드명을 비교하여 serializer field의 validation을 각각 체크하고 python native 데이터 타입으로 변환해준다. to_internal_valueto_representation의 반대 과정이라 생각하면 된다. json을 python dict으로 변환하는 과정에서 예를들어 실제 값은 정수인데 문자열 형태로 변환되는 경우가 있을 수 있는데, 이렇게 의도하지 않게 변환된 dictionary가 to_internal_value 메소드 내에서 serializer의 필드 타입에 알맞게 변환된다. 즉. 내부적으로 사용할 데이터 형태에 맞게 변환해준다.

예를들어 보통 유저 응답 스키마에 User 모델에 정의한 필드 말고도 School 모델에서 정의한 유저의 학교 정보를 추가한다고 했을 때 아래처럼 serializer를 구현할 수 있다. 사실 SerializerField 클래스를 전부 상속한 형태이고, Field 클래스에서부터 to_representationto_internal_value 메소드 구현하며 이때는 serializer의 필드 각각에 대해 (de)serialization 을 진행한다. 그리고 Serializer 클래스에서 정의한 to_representationto_internal_value 메소드는 각 필드를 돌면서 필드마다 정의된 to_representationto_internal_value 메소드를 호출한다.

class UserSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=10)
    phone_number = serializers.CharField(max_length=15)
    school = SchoolSerializer

    def create(self, validated_data):
        return User.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.name = validated_data.get('name', instance.name)
        instance.id = validated_data.get('id', instance.id)
        instance.password = validated_data.get('password', instance.password)

    def to_representation(self, instance):
        response = super().to_representation(instance)
        school = SchoolSerializer(instance.school).data
        response["school"] = school  
        return response

실제로 문서 에서 BaseSerailizer를 상속받아 override 할 수 있는 메소드 4개를 소개하고 있다.

  • .to_representation() - Override this to support serialization, for read operations.
  • .to_internal_value() - Override this to support deserialization, for write operations.
  • .create() and .update() - Override either or both of these to support saving instances.

Read only serializer를 정의하고 싶을 땐 아래처럼 to_representation 을 구현하고

class HighScoreSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        return {
            'score': instance.score,
            'player_name': instance.player_name
        }

Read write serializer를 정의하고 싶을 땐 아래처럼 to_representationto_internal_value를 함께 구현할 수 있다.


class HighScoreSerializer(serializers.BaseSerializer):
    def to_internal_value(self, data):
        score = data.get('score')
        player_name = data.get('player_name')

        # Perform the data validation.
        if not score:
            raise serializers.ValidationError({
                'score': 'This field is required.'
            })
        if not player_name:
            raise serializers.ValidationError({
                'player_name': 'This field is required.'
            })
        if len(player_name) > 10:
            raise serializers.ValidationError({
                'player_name': 'May not be more than 10 characters.'
            })

        # Return the validated values. This will be available as
        # the `.validated_data` property.
        return {
            'score': int(score),
            'player_name': player_name
        }

    def to_representation(self, instance):
        return {
            'score': instance.score,
            'player_name': instance.player_name
        }

    def create(self, validated_data):
        return HighScore.objects.create(**validated_data)