JSON 직렬화 및 API 응답 (Django vs DRF)

guava·2022년 1월 13일
0

파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 강의를 듣고 정리한 글입니다.

직렬화 (Serialization)?

모든 프로그래밍 언어의 통신에서 데이터는 필히 문자열로 표현되어야만 한다.

  • 송신자: 객체를 문자열로 변환하여 데이터 전송 → 직렬화
  • 수신자: 수신한 문자열을 다시 객체로 변환하여 활용 → 비직렬화

각 언어에서 모두 지원하는 직렬화 포맷(JSON, XML 등)도 있고 특정 언어에서만 지원하는 직렬화 포맷(파이썬은 Pickle)이 있다.

  1. 직렬화 rule이 있다면 비직렬화 rule이 반드시 존재하여야 한다.
  2. Pickle 등을 사용하면, 타 언어에서의 비직렬화 방법이 없으므로, 오류가 발생할 수 있다.

JSON 변환


데이터는 같아도 응답 형식이 다를 수 있다.

보통의 웹

  • GET 요청에 대해 HTML 포맷으로 응답
  • POST 요청을 application/x-www-form-urlencoded 인코딩 혹은 multipart/form-data 인코딩으로 요청하고, HTML 포맷으로 응답

요즘의 API 서버

  • JSON 인코딩 된 요청/응답

표준 직렬화 (JSON 포맷과 PICKLE 포맷)


JSON 포맷

  • 다른 언어/플랫폼과 통신할 때 주로 사용
  • 표준 라이브러리 json제공
    • 파이썬 기본 데이터 타입에 대해서는 어느정도 변환 룰을 제공
    • 다만, 파이썬 기본이 아닌 장고모델, 쿼리셋, 직접만든 타입 등등에 대해서는 직렬화 룰이 없다.
    • 커스텀 타입 등에 대해서는 따로 직렬화 룰을 만들면 된다.
  • pickle에 비해 직렬화를 지원하는 데이터타입의 수가 적지만, 커스텀 Rule 지정도 가능

JSON 직렬화/비직렬화 샘플 코드

  • json.dumps(post_list)실행 시 str타입으로 저장된다.
  • utf-8인코딩으로 변환하면, utf-8인코딩의 bytes가 생성이 된다.
  • json 문자열을 다른 클라이언트(iOS, Android 등)에서 수신하면, 역변환을 통해 사용이 가능하다.
import json
# 데이터 준비
post_list = [
    {'message': 'hello askdjango'},
]
# 직렬화
json_string = json.dumps(post_list) # str type
print(json_string) # '[{"message": "hello askdjango"}]'
# 비직렬화 -> 다시 객체
print(json.loads(json_string))

PICKLE 포맷

  • 파이썬 전용 포맷으로서 파이썬 시스템끼리만 통신할 때 사용 가능
  • 표준 라이브러리 pickle 제공 → json 라이브러리와 유사한 사용 방법
  • 주의) 파이썬 버전 특성을 타는 경우가 있음 → 영속성이 필요한 데이터 보다는 임시로 사용하는 데이터로 사용하는게 좋다.

PICKLE 직렬화/비직렬화 샘플 코드

  • pickle.dumps(post_list)실행 시 bytes타입으로 저장된다.
  • pickle을 통해 생성된 바이너리는 타 클라이언트(iOS, Android)등에서 수신하면 역변환 룰이 없어서 사용이 불가능하다.
import pickle
# 데이터 준비
post_list = [
    {'message': 'hello askdjango'},
]
# 직렬화
pickle_bytes = pickle.dumps(post_list) # bytes type
print(pickle_bytes) # b'\x80\x03]q\x00}q\x01X\x07\x00\x00\x00messageq\x02X\x0f\x00\x00\x00hello askdjangoq\x03sa.'
# 비직렬화 -> 다시 객체
print(pickle.loads(pickle_bytes))

TypeError: Object of type "???" is not JSON serializable

python3 manage.py shell
>>> import json
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model()
>>> json.dumps(User.objects.first())
TypeError: Object of type "???" is not JSON serializable

json/pickle 모두 파이썬 기본 라이브러리

장고 타입(Model/QuerySet 등)에 대해서는 직렬화 Rule이 없음.

Django의 직렬화


DjangoJSONEncoder를 통해 추가로 부여된 Rule

다음 타입에 대한 직렬화 Rule을 추가로 구현한다.

  • datetime.datetime, datetime.date, datetime.time, datetime.timedelta
  • decimal.Decimal, uuid.UUID, Promise

json.dumps에 cls인자로 넘겨서 사용한다. →ex. json.dumps({{ data }}, cls=DjangoJSONEncoder)

QuerySet은 직렬화가 불가능 😔

import json
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.auth import get_user_model

qs = get_user_model().objects.all()
json_string = json.dumps(qs, cls=DjangoJSONEncoder) # 위의 룰은 해결 가능하나, qs에 대해서는 동일한 오류가 발생한다.

print(json_string)

어떻게 해결할 수 있을까??

  1. 직접 파이썬 기본 객체로 변환하기
data = [
    {'id': post.id, 'title': post.title, 'content': post.content}
    for post in Post.objects.all()
]
json.dumps(data)

# PS. 추가 인자 설명
# ensure_ascii=False : 유니코드로 표현
# .encode('utf8') : 특정 인코딩으로 변환(utf-8)
json.dumps('한글', ensure_ascii=False).encode('utf8')  
  1. 직접 변환 Rule 지정하기
  • DjangoJSONEncoder에서 구현된 몇가지 타입이 있고, 그 타입에 몇가지를 더 추가한다.
  • Python에서 지원하는 직렬화 방법과 Django의 DjangoJSONEncoder를 커스텀해서 직렬화 인코더를 만든다.
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.query import QuerySet

# 커스텀 JSON Encoder를 정의
class MyJSONEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, QuerySet):
            """
            1. obj가 QuerySet type의 인스턴스인 경우에는 강제로 tuple로 변환하겠다.
            2. QuerySet 내에서도 tuple로 변환 도중에 여러 값이 있다.
            3. 2에서 변환도중의 여러 값에도 재귀적으로 이 로직을 타서 튜플로 변환한다.
            """
            return tuple(obj)
        elif isinstance(obj, Post):
            """Post모델 타입인 경우에는 아래와 같이 사전타입으로 변환하겠다."""
            return {'id': obj.id, 'title': obj.title, 'content': obj.content}
        elif hasattr(obj, 'as_dict'):
            """obj에 as_dict()라는 메소드가 있다면, 호출해서 반환값을 리턴하겠다."""
            return obj.as_dict()
        return super().default(obj)  # DjangoJSONEncoder가 구현한 기본 로직을 쓰겠다.

data = Post.objects.all()

# 직렬화할 때, 직렬화를 수행해줄 JSON Encoder를 지정해줍니다.
json.dumps(data, cls=MyJSONEncoder, ensure_ascii=False)

DRF 직렬화


  • DRF의 직렬화는 Serializer + (JSONRenderer or json.dumps() )가 함께 사용되어야 한다.
  • Serializer는 QuerySet 또는 Model을 직렬화 가능한 객체(ReturnDict or ReturnList)를 반환해주고, 이 반환된 객체를 직렬화 해주면 된다.
  • JSONRenderer는 json.dumps()에 대한 래핑 클래스 이다. 내부적으로 직렬화 Rule은 JSONEncoder를 사용하고 있다.

JSONEncoder

rest_framework.utils.encoders.JSONEncoder

  • DRF에 정의되어있는 직렬화 Rule이다. json.dumps에 cls인자로 넘겨서 사용할 수 있다.
  • 장고의 DjangoJSONEncoder를 상속받지 않고, json.JSONEncoder 상속을 통해 구현되었다.
  • datetime.datetime/date/time/timedelta, decimal.Decimal, uuid.UUID, six.binary_type를 대응한다.
  • __getitem__ 속성을 지원할 경우 dict(obj) 변환
  • __iter__ 속성을 지원할 경우, tuple 변환
  • QuerySet 타입일 경우, tuple 변환
  • .tolist 속성을 지원할 경우. obj.tolist() 반환
  • Model 타입은 미지원

JSONRenderer

rest_framework.renderer.JSONRenderer

json.dumps에 대한 래핑 클래스이다. 보다 편리한 직렬화 지원하며 UTF8 인코딩도 추가로 수행한다.

내부적으로는 JSONEncoder를 사용한다.

이를 커스텀하여 Response 스키마를 디자인하기도 하는거같다.

from rest_framework.renderers import JSONRenderer

data = Post.objects.all()
JSONRenderer().render(data)  # 오류. 
# rest_framework.utils.encoders.JSONEncoder에 정의된 Rule에 대응된 객체만 직렬화 가능.

Serializer를 통한 JSON 직렬화

Django Form 과의 비교

Serializer/ModeSerializerForm/ModelForm과 유사하다.

→ 역할 면에서 Serializer는 POST요청만 처리하는 Form이라 할 수 있다.

장고의 Form/ModelForm vs DRF의 Serializer/ModelSerializer

  • 공통점
    • 폼 필드 지정 혹은 모델로부터 읽어온다.
    • 입력된 데이터에 대한 유효성 검사 및 획득(저장)
  • 차이점
    • Form / ModelForm : Form태그가 포함된 HTML을 생성
    • Serializer / ModelSerializer : Form 데이터가 포함된 JSON 문자열을 생성

ModelSerializer

QuerySet 또는 Model를 ReturnList 또는 ReturnDict 타입으로 반환해준다.

이러한 ReturnDict 또는 ReturnList는 json직렬화가 가능한 객체이다.

ModelSerializer 정의

from rest_framework.serializers import ModelSerializer

# Post모델에 대한 ModelSerializer 정의
class PostSerializer(ModelSerializer):
    class Meta:
        model = Post
        fields = '__all__'

post = Post.objects.first() # Post 타입

serializer = PostSerializer(post)
serializer.data # -> ReturnDict 타입

ReturnDict 또는 ReturnList로 반환하기

Model 객체 및 QuerySet에 대해 ReturnDict 타입으로의 변환이 가능하다.

from myapp.serializers import PostSerializer
from myapp.models import Post

# 한개의 모델만 넘기기
serializer = PostSerializer(Post.objects.first())
serializer.data # {'id': 1, 'message': '첫번째 포스팅' ... }

# 쿼리셋을 넘기기
serializer = PostSerializer(Post.objects.all(), many=True)
serializer.data # [OrderDict([('id', 1), ...)]), OrderDict([('id': 2), ...)]) ...]

ReturnDict 타입?

  • serializer.data의 데이터 타입
  • OrderdDict(순서가 보장된 사전 타입)를 상속받았으며, serializer를 키워드 인자로 받는다.
class ReturnDict(OrderedDict):
    def __init__(self, *args, **kwargs):
        self.serializer = kwargs.pop('serializer')
        super().__init__(*args, **kwargs)

QuerySet, Model의 json 직렬화 예시

Model 객체에 대해서는 필히 many=False지정(디폴트)
QuerySet 객체에 대해서는 필히 many=True지정
→ 지정이 맞지 않으면 변환 에러 발생 (TypeError, AttributeError)

일단, ReturnDict나 ReturnList로 반환 후 직렬화를 하면 된다.

qs = Post.objects.all()
serializer = PostModelSerializer(qs, **many=True**)

serializer.data # QuerySet -> ReturnList or Model -> OrderedDict

# 파이썬 기본 JSON 변환(직렬화) 사용
import json
json_str_string = json.dumps(serializer.data, ensure_ascii=False)

# DRF에서 지원하는 JSON 변환(직렬화) 활용 -> 변환 Rule이 추가된 Encoder
from rest_framework.renderers import JSONRenderer
json_utf8_string = JSONRenderer().render(serializer.data)

Serializer를 통한 Model Instance → OrderedDict

Serializer의 to_representation 메소드에서 OrderDict로 변환하고

Serializer의 data 프로퍼티에서 ReturnDict라는 클래스로 래핑해서 반환한다.

class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
# ...
    def to_representation(self, instance):
    """
    Object instance -> Dict of primitive datatypes.
    """
    ret = OrderedDict()
    fields = self._readable_fields

    for field in fields:
        try:
            attribute = field.get_attribute(instance)
        except SkipField:
            continue

        # We skip `to_representation` for `None` values so that fields do
        # not have to explicitly deal with that case.
        #
        # For related fields with `use_pk_only_optimization` we need to
        # resolve the pk value.
        check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
        if check_for_none is None:
            ret[field.field_name] = None
        else:
            ret[field.field_name] = field.to_representation(attribute)
    return ret

View에서의 JSON 응답 (순수 Django)


장고 기본 View에서의 HttpResponse JSON 응답

  • 모든 View는 HttpResponse 타입의 응답을 해야만 한다.
  • 일반적으로 다음 2가지 방법이 있음
    1. 직접 json.dumps를 통해 직렬화된 문자열을 획득하여 HttpResponse를 통해 응답
    2. 1번을 정리하여 JsonResponse (HttpResponse를 상속받아 json.dumps로 구현된 클래스) 지원 → 내부적으로 json.dumps를 사용하며, DjangoJSONEncoder가 디폴트 지정

JsonResponse에서 QuerySet을 JSON 직렬화

위에서 정리한 MyJSONEncoder를 활용

qs = Post.objects.all()

**# JsonResponse 생성자의 각종 인자** 나열
encoder = MyJSONEncoder
safe = False # True: data가 dict일 경우, False: dict이 아닐 경우
json_dumps_params = {'ensure_ascii': False}
kwargs = {} # HttpResponse에 전해지는 Keyword 인자

from Django.http import JsonResponse
response = JsonResponse(qs, encoder, safe, json_dumps_params, **kwargs)

DRF를 통한 JSON 응답


DRF를 통한 HttpResponse JSON 응답

  • DRF Response 활용
    qs = Post.objects.all()
    
    serializer = PostModelSerializer(qs, many=True)
    
    from rest_framework.response import Response
    response = Response(serializer.data) # Content-Type: text/html 디폴트 지정

Response에서는 "JSON 직렬화"가 Lazy하게 동작한다.
실제 응답 생성 시에 .rendered_content 속성에 접근하며, 이 때 변환이 이루어진다.

Class based view : APIView

Function based view: @api_view

Response와 APIView

  • DRF의 모든 뷰는 APIView를 상속받는다.
  • APIView를 통해 Response에 다양한 속성이 지정된다.
  • APIView 로직을 타게 되면 다음과 같이 내부적으로 구현되어 있다.
from rest_framework.views import APIView

renderer_cls = APIView.renderer_classes[0]
renderer_obj = renderer_cls()
response.accepted_renderer = renderer_obj # JSON 변환을 위한 JSONRenderer 인스턴스

response.accepted_media_type = renderer_obj.media_type # 'application/json'
response.renderer_context = {'view': None, 'args': (), 'kwargs': {}, 'request': None}

response # <Response status_code=200, "application/json">

실제 DRF Serializer 활용


아래는 List기능을 지원하는 코드다. (list, create, detail, update, delete등을 모두 지원하려면 ViewSet을 활용하면 된다.)

from rest_framework import generics

class PostListAPIView(generics.ListAPIView):  # generics.ListCreateAPIView -> list, create를 지원한다.
    queryset = Post.objects.all()
    serializer_class = PostModelSerializer

post_list = PostListAPIView.as_view()  # path('public/', views.PostListAPIView.as_view()),를 urls.py의 urlpatterns에 추가해서 사용한다.

포스팅 조회 응답에 username응답을 하려면?

첫번째

  • author = FK(user)필드가 있을 때, Serializer에서는 FK키값으로 응답
  • serializer.ReadOnlyField를 통해 FK의 필드값을 읽어올 수 있습니다.
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
    username = serializers.ReadOnlyField(source='author.username') # 추가

    class Meta:
        model = Post
        fields = ['pk', 'username', 'title', 'cotnent'] # 'username' 추가

두번째

  • StringRelatedField, SlugRelatedField 등 뿐만 아니라, 중첩된 Serializer를
    통해서도 구현 가능
from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import Post
class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ['username']

class PostSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()

    class Meta:
        model = Post
        fields = '__all__'

0개의 댓글