일하는/Cloud, Web

Django REST Framework : (1) Serialization

김논리 2020. 9. 2. 15:49

원문은 Django REST Framework 공식 페이지 🏠 https://www.django-rest-framework.org/tutorial/1-serialization/ 에서 확인할 수 있습니다.


Introduction

이 튜토리얼에서는 pastebin과 같은 간단한 코드 하이라이팅 Web API를 만들어 보자.

이를 통해 DRF의 다양한 기능을 살펴보고, 각 기능들이 어떻게 결합되어 동작하는지 이해할 수 있다.

 

Setting up a new environment

항상 시작하기 전에, 가상 환경을 만들도록 한다. 이를 통해 우리의 패키지 환경이 항상 독립적으로 관리될 수 있다는 사실을 잊어서는 안 된다.

 

$ python3 -m vevn env
$ source env/bin/activate

 

새로 만든 가상 환경에 필요한 패키지들을 설치한다.

 

(env) $ pip install django
(env) $ pip install djangorestframework
(env) $ pip install pygments     # 코드 하이라이팅 기능을 위해 설치한다.

 

Getting Started

이제 새로운 Django 프로젝트와 애플리케이션을 생성해 보자.

 

(env) $ django-admin startproject tutorial
(env) $ cd tutorial
(env) tutorial $ python manage.py startapp snippets    # django-admin command를 사용해도 된다.

애플리케이션을 외부 모듈과 namespace 충돌이 발생하지 않도록, 프로젝트 내부에 생성하였다.

 

이제 생성한 snippest 애플리케이션과 DRF를 프로젝트의 INSTALLED_APP으로 추가하기 위해 tutorial/settings.py 파일을 다음과 같이 수정한다.

 

INSTALLED_APPS = [
    ...,
    'rest_framework',
    'snippets.apps.SnippetsConfig',
]

 

Creating a model to work with

이 예제에서 사용할 Snippet이 저장될 간단한 모델을 만들어 보자. Snippet 모델에는 다음과 같은 필드들이 필요하다.

  • create : date
  • title : char
  • code : text
  • linenos : boolean
  • language : char, choice
  • style : char, choice
  • owner : foreignKey auth.User
  • highlighted : text

snippets/models.py 파일을 다음과 같이 수정한다.

 

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
 
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])
 
 
class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
 
    class Meta:
        ordering = ['created']

choice 형태인 languagestyle 필드를 위해 pygments 패키지에서 필요한 내용들을 가져와 사용하였다.

 

Snippet 모델의 최초 migration을 생성하고, 데이터베이스를 동기화해 보자.

 

(env) tutorial $ python manage.py makemigrations snippets
(env) tutorial $ python manage.py migrate

makemigrations를 수행하면, snippets/migrations 디렉터리에 0001_initial.py와 같은 이름으로 migration 파일이 생성된다.

 

 

Creating a Serializer class

Web API를 만들기 위해 가장 먼저 해야 할 사항은 Snippet 인스턴스를 JSON과 같은 형태로 직렬화(Serializing)하거나 반 직렬화(Deserializing)하는 방법을 제공하는 것이다. DRF에서는 DjangoForm과 비슷한 방식으로 Serializer를 선언하여 이를 수행할 수 있다. snippets/serializers.py 파일을 생성하고, 아래와 같이 작성한다.

 

from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
 
 
class SnippetSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(required=False, allow_blank=True, max_length=100)
    code = serializers.CharField(style={'base_template': 'textarea.html'})
    linenos = serializers.BooleanField(required=False)
    language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
    style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
 
    def create(self, validated_data):
        """
        Create and return a new `Snippet` instance, given the validated data.
        """
        return Snippet.objects.create(**validated_data)
 
    def update(self, instance, validated_data):
        """
        Update and return an existing `Snippet` instance, given the validated data.
        """
        instance.title = validated_data.get('title', instance.title)
        instance.code = validated_data.get('code', instance.code)
        instance.linenos = validated_data.get('linenos', instance.linenos)
        instance.language = validated_data.get('language', instance.language)
        instance.style = validated_data.get('style', instance.style)
        instance.save()
        return instance

 

Serializer 클래스의 처음은 직렬화 또는 반직렬화될 필드 (id, title,...)를 정의하고, 그다음은 create() 메서드와 update() 메서드를 정의하였다. 이 메서드는 serializer.save()가 호출되었을 때, 인스턴스가 생성 또는 수정되는 방법을 정의하는 것이다.

 

앞서 설명한 것과 같이 Serializer 클래스는 DjangoForm 클래스와 유사하며, requiredmax_length, default와 같은 필드 유효성 검사를 위한 옵션들을 설정할 수 있다.

 

이러한 옵션들을 통해 HTML로 렌더링 할 때와 같은 특정 상황에서 Serializer가 어떻게 동작해야 하는지를 명시할 수 있다. 위 코드의 {'base_template': 'textarea.html'} 은 Django Formwidget=widget.Textarea를 사용하는 것과 같다. 이는 browsable API를 만들 때 꽤나 유용하게 사용할 수 있다.

 

다음으로 살펴볼 ModelSerializer 클래스를 사용하면, 위와 같이 하나하나 구현하지 않아도 되지만, 일단 명시적으로 Serializer를 만들어 보았다.

 

Using ModelSerializers

우리가 만든 SnippetSerializer는 Snippet 모델의 정보를 그대로 복사하여 사용하고 있다. 중복된 내용을 줄여 코드를 좀 더 간결하게 유지해 보자.

 

Django에서 Form 클래스와 ModelForm 클래스를 모두 제공하는 것과 같이, DRF에서도 Serialzier 클래스와 ModelSerializer 클래스 모두를 제공하고 있다. ModelSerialzier 클래스를 사용하여 앞서 만든 Serialzier를 리팩터링 해 보자. snippets/serializers.py 파일의 SnippetSerializer 클래스를 다음과 같이 수정해 보자.

 

from rest_framework import serializers
from snippets.models import Snippet

class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style']

 

앞서 ModelSerializer가 아닌 일반 Serialzier 클래스를 사용했을 때는 클래스의 첫 부분에 직렬화와 반직렬화가 필요한 필드들을 모두 각각 정의해야 했다. 또한, create()update() 메서드도 직접 구현해야 했다. 하지만 ModelSerializer 클래스를 사용하면, 위와 같이 아주 짧은 코드로도 동일한 동작을 수행할 수 있다.

 

ModelSerializerModel의 필드를 자동으로 인식하고, create()update() 메서드가 이미 구현되어있다.

 

Writing reqular Django views using our Serializer

새로 만든 Serializer 클래스를 이용하여 몇 개의 API View를 작성하는 방법에 대해 살펴보자.

우선 DRF의 다른 기능은 사용하지 않고 일반적인 DjangoView를 만들어 보자. snippets/views.py 파일에 다음과 같이 작성한다.

 

from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
 
@csrf_exempt
def snippet_list(request):
    """
    List all code snippets, or create a new snippet.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data, safe=False)
 
    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)

 

API의 root는 저장된 모든 Snippet을 보여주거나 (GET), 새로운 Snippet을 추가할 수 있는 (POST) 뷰가 된다.

 

snippet_list() 메서드 상단의 @csrf_exempt 데코레이터는 CSRF 토큰이 없는-인증되지 않은-사용자에게도 해당 뷰에 POST를 수행할 수 있도록 한다.

 

이 데코레이터는 일반적으로 잘 사용되지 않으며, DRF에서는 다른 속성을 통해 더 합리적으로 사용할 수 있지만, 본 예제에서 필요한 내용이므로 일단 추가해 둔다.

 

이것 외에도 개별 Snippet의 상세 내용을 볼 수 있는 (단일 Snippet에 해당하는 GET) 뷰가 필요하며, 이 코드 Snippet을 업데이트하거나 (PUT), 삭제할 수도 (DELETE) 있어야 한다. snippets/views.py 파일에 다음 내용을 추가한다.

 

def snippet_detail(request, pk):
    """
    Retrieve, update or delete a code snippet.
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return HttpResponse(status=404)
 
    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return JsonResponse(serializer.data)
 
    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(snippet, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)
 
    elif request.method == 'DELETE':
        snippet.delete()
        return HttpResponse(status=204)

 

이제 마지막으로 만든 뷰와 URL을 연결하기 위해, snippets/urls.py 파일을 생성하고 다음과 같이 작성한다.

 

from django.urls import path
from snippets import views
 
urlpatterns = [
    path(r'snippets/', views.snippet_list),
    path(r'snippets/<int:pk>/', views.snippet_detail),
]

 

또한 애플리케이션(snippets)의 URL을 프로젝트에 포함시키기 위해, tutorial/urls.py 파일을 다음과 같이 수정한다.

 

from django.urls import path, include
 
urlpatterns = [
    path(r'', include('snippets.urls')),
]

 

이 예제에서는 tutorial/urls.py 에 이미 작성되어 있는 path('admin/', admin.site.urls)를 삭제해도 무방하다.

 

Testing our first attempt at a Web API

이제 서버를 구동하고, 우리가 만든 API를 테스트해 보자. Django의 개발 서버를 실행시킨다.

 

(env) tutorial $ python manage.py runserver

 

우선, 새로운 Snippet을 POST해 보자.

 

$ http POST http://127.0.0.1:8000/snippets/ code='foo = "bar"' language=python lienos=false style=friendly title=test1
HTTP/1.1 201 Created
Content-Length: 113
Content-Type: application/json
Date: Wed, 02 Sep 2020 06:44:30 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.6.9
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "code": "foo = \"bar\"",
    "id": 1,
    "language": "python",
    "linenos": false,
    "style": "friendly",
    "title": "test1"
}

$ http POST http://127.0.0.1:8000/snippets/ code='printf("hello, world!");' language=c lienos=true style=friendly title=test2
HTTP/1.1 201 Created
Content-Length: 121
Content-Type: application/json
Date: Wed, 02 Sep 2020 06:45:58 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.6.9
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "code": "printf(\"hello, world!\");",
    "id": 2,
    "language": "c",
    "linenos": false,
    "style": "friendly",
    "title": "test2"
}

 

이제 Snippets 리스트를 GET해 보자.

 

$ http GET http://127.0.0.1:8000/snippets/
HTTP/1.1 200 OK
Content-Length: 238
Content-Type: application/json
Date: Wed, 02 Sep 2020 06:47:06 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.6.9
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
    {
        "code": "foo = \"bar\"",
        "id": 1,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": "test1"
    },
    {
        "code": "printf(\"hello, world!\");",
        "id": 2,
        "language": "c",
        "linenos": false,
        "style": "friendly",
        "title": "test2"
    }
]

 

하나의 특정 Snippet도 가져올 수 있다.

 

$ http GET http://127.0.0.1:8000/snippets/2/
HTTP/1.1 200 OK
Content-Length: 121
Content-Type: application/json
Date: Wed, 02 Sep 2020 06:47:45 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.6.9
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "code": "printf(\"hello, world!\");",
    "id": 2,
    "language": "c",
    "linenos": false,
    "style": "friendly",
    "title": "test2"
}

https://ungodly-hour.tistory.com/24

 

Django REST Framework : (2) Requests and Responses

원문은 Django REST Framework 공식 페이지 🏠 https://www.django-rest-framework.org/tutorial/2-requests-and-responses/ 에서 확인할 수 있습니다. https://ungodly-hour.tistory.com/23 Django REST Fram..

ungodly-hour.tistory.com