SW개발/Django

[Django]Django REST Framework 튜토리얼 1 (Serialization)

이번 포스팅에서는 DRF의 Serialization 공식 문서를 공부하면서 번역해보려고 한다.

해석에 틀린 내용이 있을 수 있으니 이해가 안가는 부분은 아래의 공식문서를 참조하기 바란다.

 

DRF Serialization tutorial 공식 Documentation

 

Introduction

이 튜토리얼에서는 pastebin 같이 code를 highlighting 할 수  있는 간단한 Web API를 만드는 방법을 다룬다.

그 과정에서 REST framework를 구성하는 다양한 컴포넌트들을 소개하고, 모든 것들이 어떻게 결합되는지에 대한 포괄적인 이해를 도울 것 이다.

 

이 튜토리얼은 상당히 심층적이므로 시작하기 전에 간단한 맥주와 쿠키를 가져와야 한다! 만약 quick overview를 원한다면 quickstart 세션으로 넘어가자.


참고 : 이 튜토리얼의 코드는 github의 github.com/encode/rest-framework-tutorial 레포지토리에 올라가 있다.
완성된 구현을 보고싶다면 테스트를 위한 sanbox가 온라인으로 제공된다. (restframework.herokuapp.com/) 


Setting up a new environment

다른 작업을 수행하기전에 우리는 venv를 이용하여 새로운 가상환경을 생성할 것이다. 이렇게 하면 패키지 설정이 우리가 작업중인 다른 프로젝트와는 완벽하게 분리 된 상태로 유지된다.

python3 -m venv env
source env/bin/activate

이제 가상환경 안에서 필요한 패키지들을 설치하면 된다.

pip install django
pip install djangorestframework
pip install pygments  # 코드 하이라이팅에 사용되는 패키지 설치

참고 : 언제든 가상환경에서 나가려면 deactivate를 입력하면 된다. 더 많은 정보는 venv documentation 에 있다.

 

Getting started

이제 코딩을 시작할 준비가 되었다. 시작하려면 작업 할 새로운 프로젝트를 생성하자.

cd ~
django-admin startproject tutorial
cd tutorial

우리는 간단한 Web API를 만드는데 사용할 앱을 생성할 것이다.

python manage.py startapp snippets

새로운 snippets app 과 rest framework를 tutorial/settings.pyINSTALLED_APPS에 추가하여 준다.

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

좋다, 준비가 되었다.

 

Creating a model to work with

우리는 이번 튜토리얼의 목적을 이루기 위해 code snippet을 저장하는 간단한 Snippet이라는 모델을 만들 것이다.

계속해서 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
python manage.py migrate

 

Creating a Serializer class

우리가 Web API를 시작하기 위해 가장 먼저 해야할 것은 snippet 인스턴스json 형태로 serializing, deserializing 하는 것이다.

장고의 form과 매우 유사하게 작동하는 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):
        """
        유효한 데이터가 들어오면 Snippet 인스턴스를 만들고 반환함
        """
        return Snippet.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """
        유효한 데이터가 들어오면 Snippet 인스턴스를 업데이트하고 반환함
        """
        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 클래스의 첫번째 부분은 serializer/deserializer 되는 필드를 정의한다. create(), update() 메서드는 serializer.save() 호출 시에 완전한 인스턴스가 생성되거나 수정된다.

Serializer 클래스는 Django form 클래스와 매우 유사하고 required, max_length, default 처럼 다양한 필드에 유사한 유효성 검사 플래그를 포함하고 있다.

필드 플래그는 또한 HTML로 렌더링 할때 처럼 특정한 상황에서 serializer가 표시되는 방식을 제어할 수 있다. 위에서 {'base_template' : 'textarea.html'} 플래그는 Django form 클래스의 widget=widgets.Textarea와 동일하다고 생각하면 된다.

이것은 뒤의 튜토리얼 과정에서 볼 수 있듯이 검색 가능한 API가 표시되어지는 방식을 제어하는데 특히 유용하다.

나중에 살펴 볼 것이지만 ModelSerializer 클래스를 사용해서 시간을 절약할 수도 있지만 지금은 명시적으로 Serializer를 정의할 것이다.

 

Working with Serializers 

더 진행하기 전에 우리는 새로운 Serializer 클래스를 사용하는 방법을 익힐 것이다. Django shell을 살펴보자.

python manage.py shell

shell에서 몇개의 모듈을 import 하고 작업할 코드 snippet을 만들어 볼 것이다.

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()

우리는 이제 몇개의 snippet 인스턴스를 가지고 놀 수 있다. 이 인스턴스중 하나를 선택해 serializing 해볼 것이다.

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

이 시점에서 model 인스턴스는 파이썬 기본 데이터 유형으로 변환되었다. serialization을 마무리 하기 전에 우리는 이 데이터를 json 형태로 렌더링 할 것이다.

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

Deserialization도 유사하다. 먼저 stream을 파이썬 기본 데이터 유형으로 parse 한다.

import io

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

그리고 우리는 이러한 기본 데이터 타입 유형을 완전히 채워진 object 인스턴스로 복원한다.

serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
# <Snippet: Snippet object>

API가 form을 작성하는 방법과 유사한지 보라. serializer를 사용하는 view를 작성할 때 그 유사성은 더 명확해진다.

우리는 model 인스턴스 대신에 querysets 또한 serialize 할 수 있다. 그렇게하려면 단순히 serializer 인수에 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')])]

 

Using ModelSerializers

우리의 SnippetSerializer 클래스는 Snippet 모델에도 포함된 많은 정보를 복제한다. 우리의 코드를 좀 더 간결하게 유지할 수 있다면 좋을 것이다.

Django가 Form 클래스와 ModelForm 클래스를 모두 제공하는 것처럼, REST framework도 Serializer 클래스와 ModelSerializer 클래스를 모두 포함한다.

이제 ModelSerializer 클래스를 사용하여 우리의 serializer를 리팩토링 해보자. snippets/serializers.py 파일을 다시 열고 SnippetSerializer 클래스를 다음과 같이 대체하자.

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

serializer가 갖는 한가지 좋은 점은 해당 표현을 print 하여 serializer 인스턴스의 모든 필드를 검사할 수 있다는 것이다.

python manage.py shell 명령을 통해 Djanog 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 클래스는 특별한 작업을 수행하지 않아도 단순히 serializer 클래스를 만드는 지름길이라는 것을 기억해야 한다.

  • 자동으로 결정된 필드 집합이다.
  • create()와 update()메서드를 위한 간단한 기본 구현이다.
Writing regular Django views using our Serializer

우리는 새로운 Serializer 클래스를 사용하여 API view를 작성하는 방법을 알아볼 것이다. 지금은 REST framework의 다른 기능을 이용하지 않고 단순히 일반 Django views를 사용하여 view를 작성할 것이다.

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 목록을 보여주거나, 또는 새로운 snippet을 생성할 것이다.

@csrf_exempt
def snippet_list(request):
    """
    코드 snippet의 목록을 보여주거나, 새로운 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 token을 가지고 있지 않는 사용자도 이 뷰에 POST 요청을 할 수 있도록 우리는 csrf_exempt 라는 마크를 표시해 두어야 한다.

이것은 일반적으로 수행되지는 않고, REST framework 뷰에서는 이보다 더 좋은 방식을 사용하지만 지금 당장 우리는 우리의 목적을 위해 사용하는 것 뿐이다.

또한 개별 snippet에 해당하는 스니펫을 검색하고 업데이트하고 삭제하는데 사용할 view가 필요하다.

@csrf_exempt
def snippet_detail(request, pk):
    """
    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('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]

그리고 우리의 루트 urlconf인 tutorial/urls.py에 snippet 앱의 URL설정을 연결한다. 

from django.urls import path, include

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

현재 우리가 제대로 처리하지 못하는 몇가지의 케이스가 있다는 것은 주목할만한 것이다. 만약 우리가 malformed(잘못된) 형식의 json 데이터를 보내거나 view가 처리하지 않는 메소드로 request 요청을 보내면 500 "server error" 응답 오류가 발생할 것이다. 

그래도 지금은 그냥 진행할 것이다.

 

Testing our first attempt at a Web API

이제 스니펫을 제공하는 샘플 서버를 시작할 수 있다. 

쉘에서 빠져 나온다.

quit()

그리고 장고 개발 서버를 시작한다.

python manage.py runserver

Validating models...

0 errors found
Django version 1.11, using settings 'tutorial.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

그리고 다른 터미널을 실행시켜 우리의 서버를 테스트 한다.

우리는 우리의 API 를 테스트 하기 위해 curl 또는 httpie를 사용할 수 있다. Httpie 는 파이썬으로 쓰여진 사용자 친화적인 http client 이다. 이제 설치해 보자.

pip를 이용해 httpie를 설치할 수 있다.

pip install httpie

마지막으로 우리는 모든 스니펫 목록을 얻을 수 있다.

http http://127.0.0.1:8000/snippets/

HTTP/1.1 200 OK
...
[
  {
    "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를 통해 특정한 snippet만 얻을 수도 있다.

http http://127.0.0.1:8000/snippets/2/

HTTP/1.1 200 OK
...
{
  "id": 2,
  "title": "",
  "code": "print(\"hello, world\")\n",
  "linenos": false,
  "language": "python",
  "style": "friendly"
}

마찬가지로 웹 브라우저에서 URL을 방문한다면 동일한 json 데이터가 출력되는 것을 볼 수 있다.

 

Where are we now

지금까지 잘 수행하였다. 우리는 Django Forms의 API와 유사한 Serialization API를 가지고 있고, 몇개의 Django view도 가지고 있다.

우리의 API view는 현재 json 응답을 제공하는 것 이외에 특별한 일을 하지는 않는다, 고쳐야 할 케이스들이 있지만 그래도 잘 작동하는 웹 API 이다.

우리는 part of 2 tutorial 에서 조금 더 향상 시킬 것이다.

728x90