안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

장고 (Django)

[Django] REST framework - ① Serialization

피그브라더 2020. 6. 26. 18:16

본 포스팅은 아래 링크의 내용을 나름대로 정리한 글이다.

https://www.django-rest-framework.org/tutorial/1-serialization/

 

1 - Serialization - Django REST framework

This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together. The tutorial is f

www.django-rest-framework.org

 

1. 모델 정의하기

소스 코드를 저장하기 위한 Snippet 모델을 정의해보자. 앞으로 계속 이 모델을 사용하여 설명할 것이다. 참고로 장고에서 모델을 정의할 때 Primary Key로 명시한 필드가 없다면 자동으로 Primary Key로서 id 필드가 정의된다. 데이터베이스에 모델 인스턴스가 저장될 때마다 id 필드의 값은 1씩 증가한다. 따라서 아래의 Snippet 모델은 실제로 7개의 필드를 가지고 있다고 생각해야 한다.

 

▼ 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']

 

▼ 데이터베이스 마이그레이션

python manage.py makemigrations
python manage.py migrate

 

2. 시리얼라이저 정의하기 ① : Serializer 클래스

REST API를 제공하는 장고 애플리케이션은 API를 요청한 애플리케이션과 JSON 데이터를 주고받을 수 있어야 한다. 이를 위해서는 DB 인스턴스를 JSON 데이터로 시리얼라이즈 하거나, 반대로 JSON 데이터를 DB 인스턴스로 디시리얼라이즈 할 수 있어야 한다. 이러한 목적으로 DRF가 제공하는 클래스가 바로 Serializer이며(장고의 Form 클래스와 유사), 이를 상속하여 특정 모델에 대응하는 시리얼라이저를 정의하게 된다. 이러한 시리얼라이저를 이용하면 DB 인스턴스를 JSON 데이터로 표현할 수 있고, JSON 데이터를 바탕으로 DB 인스턴스를 생성하거나 수정할 수 있다.

 

시리얼라이저에는 시리얼라이즈/디시리얼라이즈 되어야 하는 모델의 필드들이 정의된다. 대응하는 모델의 모든 필드들을 정의할 수도 있고, 일부 필드만 정의할 수도 있다. 여기에서 정의되는 필드의 값들은 시리얼라이즈 시 JSON 데이터로 표현할 때 사용이 되며, 디시리얼라이즈 시 DB 인스턴스를 생성하거나 수정할 때 사용이 된다. 그리고 각 필드에 명시되는 required, max_length, default 등의 유효성 플래그(Validation Flag)들은 디시리얼라이즈 시 해당 필드의 유효성이 어떻게 검사 되어야 하는지를 나타낸다. 더불어서, 어떤 플래그들은 HTML에 렌더링 할 때와 같이 특정한 상황에서 해당 시리얼라이저가 어떻게 디스플레이되어야 하는지 지정할 수도 있다(EX. style). 이는 Browsable API가 화면에 디스플레이되는 방식을 지정하고자 할 때 매우 유용하게 쓰인다.

 

그리고 시리얼라이저에는 create() 메소드와 update() 메소드를 정의해줘야 한다. create() 메소드는 시리얼라이저를 대상으로 save() 메소드를 호출함으로써 DB 인스턴스를 생성하고자 할 때의 동작을 정의해야 하고, update() 메소드는 시리얼라이저를 대상으로 save() 메소드를 호출함으로써 DB 인스턴스를 수정하고자 할 때의 동작을 정의해야 한다.


▼ 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')

    # Serializer.save()를 통해 DB 인스턴스를 생성할 때의 동작 정의
    # 유효성 검사를 통과한 데이터들을 바탕으로 새로운 DB 인스턴스를 생성하고 반환
    def create(self, validated_data):
        return Snippet.objects.create(**validated_data)

    # Serializer.save()를 통해 DB 인스턴스를 수정 때의 동작 정의
    # 유효성 검사를 통과한 데이터들을 바탕으로 기존의 DB 인스턴스를 수정하고 반환
    def update(self, instance, 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

 

3. 시리얼라이저 정의하기 ② : ModelSerializer 클래스

장고가 Form 클래스에 대응하는 ModelForm 클래스를 제공하는 것과 마찬가지로, DRF도 Serializer 클래스에 대응하는 ModelSerializer 클래스를 제공한다. ModelSerializer는 특별한 것이 아니다. 그저 시리얼라이저를 더욱 편하게 정의할 수 있도록 리팩토링을 해줄 뿐이다. ModelSerializer 클래스가 제공하는 대표적인 기능은 다음과 같이 두 가지이다.

 

  • 시리얼라이즈/디시리얼라이즈 되어야 하는 모델 필드들을 자동으로 정의해준다. 따라서 정의할 모델의 필드명만 명시해주면 그것들의 타입에 맞게 시리얼라이저의 필드들이 자동으로 정의가 된다.
  • create() 메소드와 update() 메소드를 가장 기본적인 형태로 이미 구현해 놓았다. 따라서 특별한 세부 구현이 필요하지 않은 경우에는 굳이 두 메소드를 정의해줄 필요가 없다. 세부 구현이 필요한 경우에는 오버라이딩을 하면 된다.

 

▼ snippets/serializers.py

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

 

▼ 시리얼라이저의 필드 정보 확인하기 (python manage.py shell)

from snippets.serializers import SnippetSerializer

serializer = SnippetSerializer()
print(repr(serializer))

 

4. 시리얼라이저 다루기

▼ 시리얼라이저 다루기 (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
import io

# Snippet DB 인스턴스 생성
snippet = Snippet(code='foo = "bar"\n')
snippet.save()

# Snippet DB 인스턴스 생성
snippet = Snippet(code='print("hello, world")\n')
snippet.save()

# Snippet DB 인스턴스 시리얼라이즈 과정 1 (DB 인스턴스 -> Python Dictionary)
serializer = SnippetSerializer(snippet)
serializer.data
# {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}

# Snippet DB 인스턴스 시리얼라이즈 과정 2 (Python Dictionary -> JSON 데이터)
content = JSONRenderer().render(serializer.data)
content
# b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'

# JSON 데이터 디시리얼라이즈 과정 1 (JSON 데이터 -> Python Dictionary)
stream = io.BytesIO(content)
data = JSONParser().parse(stream)

# JSON 데이터 디시얼라이즈 과정 2 (Python Dictionary -> DB 인스턴스)
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>

# DB 인스턴스와 같이 쿼리셋도 시리얼라이즈 가능 (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')])]

 

4. REST API 뷰 정의하기 (일반적인 장고 뷰)

이제 앞서 정의한 시리얼라이저를 바탕으로, DB 인스턴스를 대상으로 요청할 수 있는 가장 기본적인 형태의 REST API 뷰들을 정의해보도록 하자. 여기서 기본적인 형태의 REST API 뷰라 함은 DB 인스턴스의 목록 조회(List), 생성(Create), 조회(Retrieve), 수정(Update), 삭제(Delete)를 위한 뷰를 의미한다.

 

여기서 정의하는 뷰들은 시리얼라이저를 사용할 뿐, 일반적인 장고의 뷰와 크게 다르지 않다. 심지어 당장 처리하지 않은 여러 예외 케이스들도 꽤 있다. 예를 들어, 잘못된 형태의 JSON 데이터로 HTTP 요청을 보냈을 때, 혹은 뷰가 처리할 수 없는 메소드로 HTTP 요청을 보냈을 때 등등이 그렇다. 이러한 경우에는 서버 500 에러가 발생할 것이다. 그럼에도 불구하고 이렇게 뷰들을 정의한 것은 REST API 뷰의 동작 원리를 밑바닥부터 설명해주기 위함이라고 받아들이면 될 것이다. 추후 포스팅에서는 REST API 뷰를 편리하게 정의할 수 있도록 DRF가 제공하는 몇몇 기능들을 살펴볼 것이다. 그 기능들을 사용하면 REST API 뷰를 정의하는 것이 훨씬 간편해질 뿐만 아니라, 여러 예외 상황에 대한 처리도 어느 정도 자동으로 해준다. 이에 대해서는 나중에 더 자세히 알아보자.

 

참고로 @csrf_exempt는 CSRF 토큰이 없는 클라이언트에서도 POST 요청을 테스트해볼 수 있도록 하기 위한 데코레이터이다. 곧이어 아래에서 httpie라는 HTTP 클라이언트를 이용해 우리가 정의한 뷰에 테스트 요청을 보내볼 것이기 때문이다.

 

▼ 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):
    # DB 인스턴스 목록 조회
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data, safe=False)

    # DB 인스턴스 생성
    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_exempt
def snippet_detail(request, pk):
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return HttpResponse(status=404)

    # DB 인스턴스 조회
    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return JsonResponse(serializer.data)

    # DB 인스턴스 수정
    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)

    # DB 인스턴스 삭제
    elif request.method == 'DELETE':
        snippet.delete()
        return HttpResponse(status=204)

 

▼ 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),
]

 

▼ tutorial/urls.py

from django.urls import path, include

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

 

5. HTTP 클라이언트로 API 테스트해보기

우리가 정의한 REST API 뷰들이 제대로 동작하는지 한 번 테스트해보자. 이를 위해, 우선 "python manage.py runserver" 명령어로 장고 애플리케이션 서버를 실행하자. 그리고 또 다른 터미널을 켜서 그곳에서 HTTP 클라이언트를 통해 우리의 서버에 HTTP 요청을 보내보자. 그러려면 먼저 HTTP 클라이언트를 설치해야 한다. 여러 종류의 HTTP 클라이언트가 있지만, 그중에서 우리는 파이썬으로 만들어진 httpie라는 HTTP 클라이언트를 사용해볼 것이다. "pip install httpie" 명령어로 httpie 파이썬 패키지를 설치하자.

 

이제 다음과 같이 입력하여 HTTP 요청을 보내보자. 뷰가 제대로 동작한다면 우리가 요청한 DB 인스턴스(들)의 정보가 JSON 형태의 응답으로 날아올 것이다. 마찬가지로, 웹 브라우저에서 아래의 URL로 접속하더라도 동일한 JSON 데이터가 디스플레이될 것이다.

 

  • http http://127.0.0.1:8000/snippets/
  • http http://127.0.0.1:8000/snippets/0/

 

6. 마무리의 말

이번 포스팅에서는 모델과 그 모델에 대응하는 시리얼라이저를 정의하고, 이를 이용한 REST API 뷰들을 정의해 보았다. 그러나 이 뷰들은 시리얼라이저를 이용하여 JSON 데이터로 응답을 보내주도록 할 뿐, 일반적인 장고의 뷰와 크게 다르지 않다. 심지어 몇몇 예외 케이스에 대한 처리도 되어 있지 않다. 다음 포스팅에서는 이러한 것들을 조금 더 개선하기 위한 내용을 다뤄보도록 하자.