DRF3 튜토리얼 1 - 직렬화

원문 - Tutorial 1: Serialization

번역을 허락해 준 Tom Christie에게 고마움을 전합니다.


튜토리얼 1: Serialization

개요

이 튜토리얼에서는 pastebin 같이 간단한 코드 하이라이팅 웹 API를 만들어 보겠습니다. 이와 함께 REST 프레임워크의 다양한 기능을 살펴보고, 이런 기능들이 어떻게 서로 어울려 작동하는지도 설명하겠습니다.

이 튜토리얼은 꽤 길기 때문에 쿠키와 음료를 옆에 준비해두기를 권합니다. 그저 슬쩍 훑어보고만 싶다면 훑어보기 문서를 보세요.


안내: 이 튜토리얼에서 사용하는 코드는 GitHub에 있는 저장소(tomchristie/rest-framework-tutorial)에서 확인할 수 있습니다. 전체 코드를 구현한 데모 버전은 여기서 확인할 수 있습니다.


새 가상 환경 만들기

뭔가를 하기 전에 virtualenv를 사용하여 가상 환경을 만듭시다. 이를 통해 패키지 설정을 독립적으로 관리할 수 있습니다.

virtualenv env  
source env/bin/activate  

가상 환경 안에서 다음과 같은 패키지를 설치합니다.

pip install django  
pip install djangorestframework  
pip install pygments  # 코드 하일라이팅에 사용할 패키지입니다  

안내: 가상 환경에서 벗어나고 싶다면 deactivate를 입력하면 됩니다. 더 자세한 내용은 virtualenv 문서에서 확인하세요.

시작하기

자, 이제 코드를 작성해 봅시다. 우선 새 프로젝트를 만듭니다.

cd ~  
django-admin.py startproject tutorial  
cd tutorial  

그리고 웹 API를 위한 앱을 하나 생성합니다.

python manage.py startapp snippets  

방금 만든 snippets 앱 외에 rest_framework 앱도 INSTALLED_APPS에 추가해야 합니다. tutorial/settings.py 파일을 수정하세요.

INSTALLED_APPS = (  
    ...
    'rest_framework',
    'snippets',
)

URL도 설정해야 합니다. tutorial/urls.py 파일에 snippet 앱의 URL을 추가합시다.

urlpatterns = [  
    url(r'^', include('snippets.urls')),
]

이제 준비 과정은 끝났습니다.

모델 만들기

이 튜토리얼에서 사용할 간단한 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  

시리얼라이저 클래스 만들기

웹 API를 만들려면 우선 Snippet 클래스의 인스턴스를 json 같은 형태로 직렬화(serializing)하거나 반직렬화(deserializing)할 수 있어야 합니다. Django REST 프레임워크에서는 Django 폼과 비슷한 방식으로 시리얼라이저를 작성합니다. snippets/serializers.py 파일을 만들고 다음과 같은 내용을 작성합시다.

from django.forms import widgets  
from rest_framework import serializers  
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.Serializer):  
    pk = 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

시리얼라이저 클래스의 윗부분에서는 직렬화/반직렬화될 필드를 선언했습니다. create() 메서드와 update() 메서드에서는 serializer.save()가 호출되었을 때 인스턴스가 생성 혹은 수정되는 과정을 전부 명시하고 있습니다.

시리얼라이저 클래스는 Django의 Form 클래스와 매우 비슷하며, requiredmax_length, default 같이 Form 클래스에서 사용하던 값 검증을 위한 옵션도 필드에 지정할 수 있습니다.

이러한 옵션들을 통해 특정 상황 - 예를 들면 HTML로 렌더링한다든지 - 에 시리얼라이저가 어떻게 작동해야 하는지를 명시할 수 있습니다. 앞선 코드의 {'base_template': 'textarea.html'} 부분은 Django Formwidget=widgets.Textarea과 같습니다. 탐색가능한 API를 만들 때 이런 기능이 얼마나 유용한지는 차차 알아보겠습니다.

사실, 나중에 살펴 볼 ModelSerializer 클래스를 사용하면 이러한 기능을 일일이 구현하지 않아도 되지만 일단은 명시적인 시리얼라이저를 만들어 봅시다.

시리얼라이저 사용하기

진도를 나아가기에 앞서, 우리가 만든 시리얼라이저 클래스에 조금 익숙해져 봅시다. Django 셸을 띄우세요.

python manage.py 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 인스턴스가 만들어졌으니, 이 인스턴스 중 하나를 직렬화해보죠.

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

여기서는 모델 인스턴스를 파이썬의 데이터 타입으로 변환했는데요. 직렬화 과정을 마무리하려면 이 데이터를 json으로 변환해야 합니다.

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

반직렬화도 이와 비슷합니다. 먼저, 파이썬 데이터 타입을 파싱합니다.

from django.utils.six import BytesIO

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

다음으로 이 데이터를 인스턴스화합니다.

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>

폼을 다루는 방식과 많이 비슷하죠? 뷰를 작성할 때뿐만 아니라 시리얼라이저를 사용하는 방식도 폼을 다루는 방식과 유사합니다.

모델의 인스턴스뿐만 아니라 쿼리셋도 직렬화할 수 있습니다. 시리얼라이저의 인자에 many=True만 추가하면 됩니다.

serializer = SnippetSerializer(Snippet.objects.all(), many=True)  
serializer.data  
# [{'pk': 1, 'title': u'', 'code': u'foo = "bar"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}, {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}]

ModelSerializers 사용하기

SnippetSerializer 클래스는 Snippet 모델의 정보들을 그대로 베낍니다. 이런 특징을 살려 코드를 좀더 간단하게 줄여 보겠습니다.

Django에서 Form 클래스와 ModelForm 클래스를 제공하듯, REST 프레임워크에서도 Serializer 클래스와 ModelSerializer 클래스를 제공합니다.

우리가 만든 시리얼라이저가 ModelSerializer 클래스를 사용하도록 리팩터링해 봅시다. snippets/serializers.py 파일을 열고 SnippetSerializer 클래스 부분을 다음과 같이 수정합니다.

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

이렇게 시리얼라이저에 프로퍼티 하나만 정의한 후 시리얼라이저 인스턴스를 출력해보면 모든 필드를 확인할 수 있습니다. python manage.py shell 명령으로 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() 메서드가 이미 구현되어 있다.

시리얼라이저를 사용하는 Django 뷰 만들기

이제, 앞에서 새로 만든 시리얼라이저 클래스를 뷰에서 어떻게 사용할 수 있는지 보여드리겠습니다. 지금 당장은 REST 프레임워크의 기능을 사용하지 않고, 일반적인 Django 뷰의 형태로 만들겠습니다.

HttpResponse의 하위 클래스를 만들고, 받은 데이터를 모두 json 형태로 반환합시다.

snippets/views.py 파일을 열고 다음 내용을 입력합니다.

from django.http import HttpResponse  
from django.views.decorators.csrf import csrf_exempt  
from rest_framework.renderers import JSONRenderer  
from rest_framework.parsers import JSONParser  
from snippets.models import Snippet  
from snippets.serializers import SnippetSerializer

class JSONResponse(HttpResponse):  
    """
    콘텐츠를 JSON으로 변환한 후 HttpResponse 형태로 반환합니다.
    """
    def __init__(self, data, **kwargs):
        content = JSONRenderer().render(data)
        kwargs['content_type'] = 'application/json'
        super(JSONResponse, self).__init__(content, **kwargs)

우리가 만들 API의 최상단에서는 저장된 코드 조각을 모두 보여주며, 새 코드 조각을 만들 수도 있습니다.

@csrf_exempt
def snippet_list(request):  
    """
    코드 조각을 모두 보여주거나 새 코드 조각을 만듭니다.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JSONResponse(serializer.data)

    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)

인증되지 않은 사용자도 이 뷰에 POST를 할 수 있도록 csrf_exempt 데코레이터를 적어둔 점을 눈여겨 보시기 바랍니다. 이는 보통의 경우 필요 없을 수도 있고 REST 프레임워크의 뷰가 이보다 더 정밀한 속성들을 제공하기도 하지만, 일단 여기서는 우리가 구현하고 싶은 기능을 csrf_exempt가 잘 담당하고 있습니다.

이제 코드 조각 하나를 보여줄 뷰도 필요합니다. 또 이 코드 조각을 업데이트하거나 삭제할 수도 있어야 합니다.

@csrf_exempt
def snippet_detail(request, pk):  
    """
    코드 조각 조회, 업데이트, 삭제
    """
    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.conf.urls import url  
from snippets import views

urlpatterns = [  
    url(r'^snippets/$', views.snippet_list),
    url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
]

여기서 다루지 않은 특별한 경우들은 그냥 무시하고 지나갑시다. json의 내용이 깨졌거나 뷰가 처리할 수 없는 메서드 요청인 경우, '500 서버 오류'를 보게 될 겁니다. 그렇지만 일단은 그냥 둡시다.

첫 번째 웹 API 테스트하기

이제 코드 조각을 보여주는 서버를 구동해 봅시다.

셸에서 빠져나가,

quit()  

Django의 개발 서버를 띄웁니다.

python manage.py runserver

Validating models...

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

다른 터미널 창에서 서버를 테스트합시다. 테스트에는 curl이나 httpie를 사용할 수 있습니다. Httpie는 파이썬으로 작성된 사용자 친화적인 http 클라이언트입니다. 설치해 보죠.

httpie는 pip로 설치하면 됩니다.

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를 지정하여 특정 코드 조각만 가져올 수도 있습니다.

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

웹 브라우저에서도 똑같은 json 데이터를 확인하실 수 있을 겁니다.

지금 어디까지 왔을까요?

지금까지 잘 해 왔습니다. 시리얼라이저 API를 만들 땐 Django 폼 API나 일반적인 Django 뷰와 비슷하다고 느꼈을 겁니다.

우리가 만든 API 뷰는 아직까진 json을 응답하는 일 외에는 별다른 일을 하지 않으며, 몇몇 경우에는 (웹 API의 기능임에도) 에러도 발생하고 있습니다.

튜토리얼 2부에서는 API를 개선해보겠습니다.