9. Django Rest Framework (1) - Serializer

bruce1115·2023년 12월 3일
0

EduMax

목록 보기
10/12
post-thumbnail

이번 글에서는 지난 글에서 이야기했던 것처럼 Django Rest Framework에 대해 공부하고, 이를 현재 있는 model들에 적용해 볼 것이다. 이 글은 Django Rest Framework 공식 문서를 참조하였다.

먼저 pip install djangorestframework 명령어를 이용하여 Django Rest Framework를 설치해 주자. 그리고 나서 Installed_app에 아래 코드와 같이 'rest_framework'를 추가해 주도록 하자.

INSTALLED_APPS = [
    "account.apps.AccountConfig",
    "community.apps.CommunityConfig",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    'rest_framework',
]

이제 Django Rest Framework를 사용할 준비가 완료되었다.

Serializer란?

앞선 글에서, REST 원칙을 어느 정도 지키는 HTTP API를 설계하였다. 그리고 여기서 데이터는 JSON 형태로 주고받는 것으로 결정하였다. 즉, API 서버에서는 DB 내의 데이터를 최종적으로 JSON 형태로 바꾸어서 네트워크를 통해 전송하고, 반대로 JSON 형태로 받아온 데이터를 다시 DB에 저장할 수 있는 객체 형태로 바꾸어서 저장해야 한다는 뜻이다. Django Rest Framework에서는 Serializer에 대해 다음과 같이 설명해놓고 있다.

Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types.

즉, Django의 queryset과 같은 복잡한 데이터들을 Python datatype으로 바꿔서 JSON 형태로 바꾸기 쉽도록 하는 역할을 해 주는 것이 Serializer의 역할이다.
왜 JSON으로 바꾸는 것을 Serialize라고 할 수 있을까? 그것은 아마도 JSON이 문자열 기반의 데이터 포맷이어서가 아닐까 생각해 본다. 단순히 데이터의 형태이기 때문에 하나의 연속된 String으로 표현되는 JSON은 전송도 용이하고, 알아보기도 쉽다. 연속적인 구조로 객체를 변경하기 때문에 Serialize라고 할 수 있는 것 같다. 원래 Serialize가 JVM 쪽에서 많이 사용되는 개념인 것 같은데, 그 부분은 내가 잘 다뤄 본 적이 없어서 와닿는 부분이 많이 없었다.

Serializer 구현해 보기

이제 코드로 Serializer를 구현해 볼 차례이다. 공식 문서의 Serializer 파트를 참조하여 다음과 같이 Post에 대한 serializer를 구현해 보았다.

from rest_framework import serializers
from .models import Post


class PostSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=30)
    content = serializers.TextField()
    created_at = serializers.DateTimeField()
    category = serializers.CharField(
        max_length=2, choices=Post.Categories.choices, default=Post.Categories.FREE
    )

model의 선언과 거의 유사하다. model에 존재하는 field를 똑같이 받아서 정의해 준 것에 불과하기 때문이다. 이 serializer에 Post 객체를 넘겨 준 뒤 json으로 변환해 주면 잘 변환되는 것을 확인할 수 있다. 아래의 코드를 보자.

from rest_framework.renderers import JSONRenderer
from community.serializers import PostSerializer
from community.models import Post
from django.utils import timezone

p = Post(title="1", content="c1", created_at=timezone.now())
serializer = PostSerializer(p)
json = JSONRenderer().render(serializer.data)
print(json)
# '{"title":"1","content":"c1","created_at":"2023-12-03T05:33:47.439757Z","category":"FR"}'

하지만 여기에는 빠진 데이터가 있다. author 데이터는 User를 foreign key로 참조하는 field였다. 이렇게 다른 모델을 참조한 field의 경우, serializer를 어떻게 구성해야 할까? 만약 서버에서 데이터를 보내는 경우, author field에 user의 정보까지 전부 다 담아서 보내 주어야 한다. 이 경우 Nested Serializer, 즉 중첩해서 serializer를 작성하면 된다. User에 대한 serializer도 만들어 주자.

from rest_framework import serializers
from .models import Post


class UserSerializer(serializers.Serializer):
    username = serializers.CharField(max_length=20)  # login ID 역할을 한다.
    email = serializers.EmailField(max_length=255)
    nickname = serializers.CharField(max_length=10)


class PostSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=30)
    content = serializers.CharField()
    created_at = serializers.DateTimeField()
    category = serializers.CharField(max_length=2, default=Post.Categories.FREE)
    author = UserSerializer()

이렇게 User의 데이터는 UserSerializer를 통해서 따로 가져온다. 이를 실행시키는 코드는 아래에 있다.

from rest_framework.renderers import JSONRenderer
from community.serializers import PostSerializer, UserSerializer
from community.models import Post
from account.models import User
from django.utils import timezone

user1 = User.objects.get(id=1)
p = Post(title="1", content="c1", created_at=timezone.now(), author=user1)
serializer = PostSerializer(p)
json = JSONRenderer().render(serializer.data)
print(json)
# b'{"title":"1","content":"c1","created_at":"2023-12-03T08:17:54.666130Z","category":"FR","author":
# {"username":"Admin","email":"bruce1115@naver.com","nickname":""}}'

User의 원하는 정보까지 한 번에 가져올 수 있게 되었다. user1은 사전에 DB에 따로 만들어 놓은 임시 데이터이다.

이제 반대로 Client에게서 Post 요청으로 JSON 데이터를 받아올 때를 생각해 보자. 먼저 JSON 데이터를 JSONParser를 이용하여 python data type으로 바꿔 준다. 그 다음에 이 데이터를 serializer의 data=를 이용하여 deserialize해 주면 된다. is_valid를 통해 유효성 검사를 할 수 있고, 유효성 검사를 마치면 해당 데이터는 serializer.validated_data에 저장된다.

ModelSerializer

사실 Model에 대한 Serialize는 ModelSerializer를 통해 더욱 쉽게 할 수 있다. 아까 만들었던 PostSerializer 역시 ModelSerializer를 통해 간편하게 쓸 수 있다.

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["title", "content", "created_at", "category", "author"]
user1 = User.objects.get(id=1)
p = Post(title="1", content="c1", created_at=timezone.now(), author=user1)
serializer = PostSerializer(p)
json = JSONRenderer().render(serializer.data)
print(json)
# b'{"title":"1","content":"c1","created_at":"2023-12-03T10:28:15.091859Z","category":"FR","author":1}'

결과에서, author에 User의 primary key 값인 1만 나오는 문제가 있다. User의 전체 데이터를 전부 가져올 수 있도록 하기 위해서는 어떻게 해야 할까? 공식 문서에서는 Meta class에 depth를 추가하면 된다고 나와 있다.

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["title", "content", "created_at", "category", "author"]
        depth = 1

하지만 이렇게 하면 필요한 데이터 뿐만 아니라 password 같은 필요 없는 데이터도 가져오게 된다. 따라서 author field를 UserSerializer를 이용하도록 명시해 주어야 한다.

from rest_framework import serializers
from .models import Post
from account.models import User


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "username", "email", "nickname"]


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

    class Meta:
        model = Post
        fields = ["title", "content", "created_at", "category", "author"]

이렇게 할 경우 depth 설정이 없어도 User의 원하는 데이터들을 읽어 올 수 있게 된다.
ModelSerializer를 이용하여 JSON 데이터를 받아 올 때는 어떨까? 역시 parser를 먼저 거친 뒤 Serializer에 data=를 이용하여 넣어 준다. 이 Serializer를 이용해 데이터를 받아와서 save까지 진행해 보자.

jsonData = b'{"title":"1","content":"c1","created_at":"2023-12-03T10:28:15.091859Z","category":"FR","author":1}'
stream = io.BytesIO(jsonData)
data = JSONParser().parse(stream)

serializer = PostSerializer(data=data)
print(serializer.is_valid(raise_exception=True))
print(serializer.validated_data)
serializer.save()

print(Post.objects.all())

이렇게 할 경우, rest_framework.exceptions.ValidationError: {'author': {'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got int.', code='invalid')]}} 에러가 나온다. 즉 author가 user이므로 dictionary를 받아야 하는데, primary key 값만 들어왔다는 것이다. Post를 업데이트하는데 user 정보 전체를 전해 주는 것은 비효율적이므로, primary key인 id 값만을 이용해 Post를 업데이트하려면 Serializer를 다음과 같이 수정해 주어야 한다.

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "username", "email", "nickname"]


class PostSerializer(serializers.ModelSerializer):
    author = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())

    class Meta:
        model = Post
        fields = "__all__"

다음과 같이 author 필드를 PrimaryKeyRelatedField를 이용하여 정의하고 queryset으로 User임을 전해 주었다. 이제 다시 위 코드를 실행할 경우 save까지 정상적으로 완료되는 것을 확인할 수 있다.

PrimaryKeyRelatedField를 쓰면서 아까 쓰인 코드와 동일하게 JSON 데이터를 보내면 author의 primary key 정보만 전달된다. 이 문제를 해결하려면 일단 가장 먼저 생각나는 것은 JSON 데이터를 보낼 때와 받을 때의 Serializer를 따로 쓰는 것이지만, 기본적으로 두 상황을 모두 지원하는데 이렇게 하는 것은 본래의 의도에도 맞지 않는다.
어떻게 Serializer를 수정해야 할 지 생각이 나지 않아서 UserSerializer과 PostSerializer를 따로 호출하여 두 객체를 합치자는 생각을 했다. 하지만 serializer의 data type은 ReturnDict로, property이기 때문에 기본적으로 immutable하다. 따라서 이 문제를 해결하기 위해 아래 코드와 같이 새로운 dictionary에 데이터를 복사하여 JSON으로 바꾸었다.

user1 = User.objects.get(id=1)
p = Post(id=1, title="1", content="c1", created_at=timezone.now(), author=user1)
serializer = PostSerializer(p)
userSerializer = UserSerializer(user1)
returnDict = {}
returnDict.update(serializer.data)
returnDict.update(author=userSerializer.data)
print(returnDict)
json = JSONRenderer().render(returnDict)
print(json)
# b'{"id":1,"author":{"id":1,"username":"Admin","email":"bruce1115@naver.com","nickname":""},"title":"1","content":"c1","created_at":"2023-12-03T14:42:21.131686Z","category":"FR"}'

기본적으로 정의되어 있는 create나 update를 오버라이딩 하는 방법도 있겠지만, 실제로 서버를 짤 때 고민해 보기로 하였다.

다음 목표

JSON 처리를 위해 반드시 필요한 Serializer에 대한 기초를 어느 정도 잡았으니 이제 이를 View에 적용해야 한다. 따라서 먼저 Django Rest Framework의 Response와 Request에 대해 공부해 보고, 이를 View에 적용하는 방법을 본격적으로 공부해 볼 예정이다.

profile
백엔드 개발자 꿈나무

0개의 댓글