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
- 시작하기에 앞서 새 프로젝트를 만들어보자.
- 프로젝트를 생성할 디렉터리에 접근한 후 다음 명령를 실행한다.
- 위의 명령이 완료되면 웹 API를 만드는 데 사용할 애플리케이션을 만들 수 있다.
- 새로운
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
모델에 대한 초기 마이그레이션을 생성하고 데이터베이스를 동기화해야 한다.
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_length
및default
와 같은 다양한 필드에 유사한 유효성 검사 플래그를 포함한다.
- 필드 플래그는 HTML로 렌더링할 때와 같은 특정 상황에서 직렬 변환기가 표시되는 방식을 제어할 수 있다.
- 위의
{"base_template": "textarea.html"}
플래그는 DjangoForm
클래스에서widget=widgets.Textarea
를 사용하는 것과 같다.
- 나중에 보게 될
ModelSerializer
클래스를 사용하여 실제로 시간을 절약할 수도 있지만 지금은 직렬 변환기 정의를 명시한다.
5. Working with Serializers
- 더 진행하기 전에 새로운
Serializer
클래스를 사용하는 방법에 익숙해질 것이다. - Django 쉘로 들어가보자.
- 먼저 다음과 같이 작업을 진행한다.
>>> 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 쉘로 다시 들어가보자.
- 다음과 같이 작성한다.
>>> 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 개발 서버를 실행해 보자.
- 다른 터미널 창을 열어
curl
또는httpie
를 사용하여 API를 테스트를 할 수 있다. - Httpie는 Python으로 작성된 사용자 친화적인 http 클라이언트이다.
- 다음과 같이 Httpie를 설치한다.
- 다음 명령을 입력하여 테스트 결과를 확인해 보자.
[{"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/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를 사용했을 때 더 보기가 편하다.