Skip to content

3. Serialization


1. Setting up a new environment

  • 튜토리얼을 위한 가상환경을 생성한 후 Django 및 Django REST framework, 기타 패키지를 설치한다.


python -m pip install Django
pip install djangorestframework
pip install pygments # We'll be using this for the code highlighting


2. Getting started

  • 시작하기에 앞서 새 프로젝트를 만들어보자.
  • 프로젝트를 생성할 디렉터리에 접근한 후 다음 명령를 실행한다.


django-admin startproject tutorial
cd tutorial


  • 위의 명령이 완료되면 웹 API를 만드는 데 사용할 애플리케이션을 만들 수 있다.


python manage.py startapp snippets


  • 새로운 snippets 앱과 rest_framework 앱을 tutorial/settings.py 파일 내의 INSTALLED_APPS 설정에 추가해야 한다.


tutorial/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "snippets.apps.SnippetsConfig",
]


3. Creating a model to work with

  • 이 튜토리얼의 목적을 위해 코드 스니펫을 저장하는 데 사용되는 간단한 Snippet 모델을 만드는 것으로 시작한다.
  • 먼저 snippets/models.py 파일에 다음과 같이 작성한다.


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"]


  • 처음에는 Snippet 모델에 대한 초기 마이그레이션을 생성하고 데이터베이스를 동기화해야 한다.


python manage.py makemigrations snippets
Migrations for 'snippets':
  snippets/migrations/0001_initial.py
    - Create model Snippet
python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, snippets
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying snippets.0001_initial... OK


4. Creating a Serializer class

  • 웹 API를 시작하기 위해 가장 먼저 해야 할 일은 스니펫 인스턴스를 json과 같은 표현으로 직렬화 및 역직렬화하는 방법을 제공하는 것이다.
  • Django의 형식과 매우 유사하게 작동하는 직렬 변환기를 선언하여 이를 수행할 수 있다.
  • snippets/serializers.py 파일을 생성하여 다음과 같이 작성한다.


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


  • 직렬 변환기 클래스의 첫 번째 부분은 직렬화 및 역직렬화되는 필드의 정의한다.
  • create()update() 메서드는 serializer.save()를 호출할 때 완전한 인스턴스가 생성되거나 수정되는 방법을 정의한다.


  • 직렬 변환기 클래스는 Django Form 클래스와 매우 유사하며 required, max_lengthdefault와 같은 다양한 필드에 유사한 유효성 검사 플래그를 포함한다.


  • 필드 플래그는 HTML로 렌더링할 때와 같은 특정 상황에서 직렬 변환기가 표시되는 방식을 제어할 수 있다.
  • 위의 {"base_template": "textarea.html"} 플래그는 Django Form 클래스에서 widget=widgets.Textarea를 사용하는 것과 같다.


  • 나중에 보게 될 ModelSerializer 클래스를 사용하여 실제로 시간을 절약할 수도 있지만 지금은 직렬 변환기 정의를 명시한다.


5. Working with Serializers

  • 더 진행하기 전에 새로운 Serializer 클래스를 사용하는 방법에 익숙해질 것이다.
  • Django 쉘로 들어가보자.


python manage.py shell


  • 먼저 다음과 같이 작업을 진행한다.


>>> from snippets.models import Snippet
>>> from snippets.serializers import SnippetSerializer
>>> from rest_framework.renderers import JSONRenderer
>>> from rest_framework.parsers import JSONParser

>>> snippet = Snippet(code='foo = "bar"\n')
>>> snippet.save()

>>> snippet = Snippet(code='print("hello, world")\n')
>>> snippet.save()


  • 이제 몇 가지 스니펫 인스턴스가 생성되었다.
  • 이러한 인스턴스 중 하나를 직렬화하는 방법을 살펴보자.


>>> serializer = SnippetSerializer(snippet)
>>> serializer.data
{'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}

>>> isinstance(serializer.data, dict)
True


  • 이 시점에서 모델 인스턴스를 Python 기본 데이터 타입 dict으로 변환했다.
  • 직렬화 프로세스를 완료하기 위해 데이터를 json으로 렌더링한다.


>>> content = JSONRenderer().render(serializer.data)
>>> content
b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'

>>> isinstance(content, (bytes, bytearray))
True


  • 역직렬화도 비슷하다.
  • 먼저 스트림을 Python 기본 데이터 타입 dict로 파싱한다.


>>> import io

>>> stream = io.BytesIO(content)
>>> data = JSONParser().parse(stream)

>>> isinstance(data, dict)
True


  • 그런 다음 해당 기본 데이터 타입 dict를 완전히 채워진 객체 인스턴스로 복원한다.


>>> serializer = SnippetSerializer(data=data)

>>> serializer.is_vaild()
True

>>> serializer.validated_data
OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])

>>> serializer.save()
<Snippet: Snippet object>


  • 모델 인스턴스 대신 쿼리 세트를 직렬화할 수 있는데, 그렇게 하려면 직렬 변환기 인수에 many=True 플래그를 추가하면 된다.


>>> serializer = SnippetSerializer(Snippet.objects.all(), many=True)

>>> serializer.data
[OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]


6. Using ModelSerializers

  • SnippetSerializer 클래스는 Snippet 모델에도 포함된 많은 정보를 복제하고 있다.
  • 코드를 좀 더 간결하게 만들어보자.


  • Django가 Form 클래스와 ModelForm 클래스를 모두 제공하는 것과 같은 방식으로 REST framework에는 Serializer 클래스와 ModelSerializer 클래스가 모두 포함된다.


  • ModelSerializer 클래스를 사용하여 직렬 변환기를 리팩토링하는 방법을 살펴보자.
  • snippets/serializers.py 파일을 다시 열고 SnippetSerializer 클래스를 다음과 같이 수정한다.


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


  • 직렬 변환기의 한 가지 좋은 속성은 해당 표현(repr())을 인쇄(print())하여 직렬 변환기 인스턴스의 모든 필드를 검사할 수 있다는 것이다.
  • Django 쉘로 다시 들어가보자.


python manage.py shell


  • 다음과 같이 작성한다.


>>> from snippets.serializers import SnippetSerializer

>>> serializer = SnippetSerializer()

>>> print(repr(serializer))
SnippetSerializer():
   id = IntegerField(label='ID', read_only=True)
   title = CharField(allow_blank=True, max_length=100, required=False)
   code = CharField(style={'base_template': 'textarea.html'})
   linenos = BooleanField(required=False)
   language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
   style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...


  • ModelSerializer 클래스는 자동으로 필드 집합을 결정하고, create()update() 메서드를 구현한다.
  • 즉, 직렬 변환기 클래스를 생성하기 위한 지름길이다.


7. Writing regular Django views using our Serializer

  • 새 직렬 변환기 클래스를 사용하여 일부 API 뷰를 작성하는 방법을 살펴보자.
  • snippets/views.py 파일을 열어 다음을 추가한다.


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


  • 다음과 같이 작성된 API의 루트는 모든 기존 스니펫을 나열하거나 새 스니펫을 생성하는 것을 지원하는 뷰가 될 것이다.


snippets/views.py
@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)


  • 현재, CSRF 토큰이 없는 클라이언트에서 이 뷰로 POST 방식으로 요청할 수 있기를 원하기 때문에 뷰 함수에 csrf_exempt 데코레이터를 달아준다.


  • 또한 개별 스니펫을 검색, 업데이트 또는 삭제하는 데 사용할 수 있는 뷰가 필요하기 때문에 다음과 같이 작성한다.


snippets/views.py
@csrf_exempt
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 파일을 생성한 후 다음과 같이 작성한다.


snippets/urls.py
from django.urls import path
from snippets import views

urlpatterns = [
    path("snippets/", views.snippet_list),
    path("snippets/<int:pk>/", views.snippet_detail),
]


  • 또한 스니펫 앱의 URL을 프로젝트와 연결해야 한다.
  • tutorial/urls.py 파일에서 다음과 같이 작성하여 스니펫 앱의 URLConf를 연결한다.


tutorial/urls.py
from django.urls import path, include

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


  • 현재 상황에서 잘못된 형식의 json을 보내거나 뷰가 처리하지 않는 메서드로 요청이 이루어진 경우 서버 오류(500) 응답으로 끝난다.


8. Testing our first attempt at a Web API

  • Django 개발 서버를 실행해 보자.


python manage.py runserver


  • 다른 터미널 창을 열어 curl 또는 httpie를 사용하여 API를 테스트를 할 수 있다.
  • Httpie는 Python으로 작성된 사용자 친화적인 http 클라이언트이다.
  • 다음과 같이 Httpie를 설치한다.


pip install httpie


  • 다음 명령을 입력하여 테스트 결과를 확인해 보자.


curl http://127.0.0.1:8000/snippets/
[{"id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly"}, {"id": 2, "title": "", "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly"}, {"id": 3, "title": "", "code": "print(\"hello, world\")", "linenos": false, "language": "python", "style": "friendly"}]
http http://127.0.0.1:8000/snippets/
HTTP/1.1 200 OK
Content-Length: 354
Content-Type: application/json
Date: Thu, 16 Dec 2021 07:31:16 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.8
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
    {
        "code": "foo = \"bar\"\n",
        "id": 1,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
    {
        "code": "print(\"hello, world\")\n",
        "id": 2,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
    {
        "code": "print(\"hello, world\")",
        "id": 3,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    }
]


  • Httpie를 사용했을 때 더 보기가 편하다.

References