Search

[drf] serializer 의 필요성과 기능들

타입
스터디
태그
drf
django
상태
Published
생성일
2023/02/27 09:59
최종 편집 일시
2023/07/18 01:44
2 more properties

Serializer 의 뜻과 존재 이유

Serialize 의 뜻을 검색해보니까 직렬화 라고 한다. 하지만 뜻을 들어도 잘 와닿지 않았다..
직렬화(直列化) 또는 시리얼라이제이션(serialization)은 컴퓨터 과학의 데이터 스토리지 문맥에서 데이터 구조나 오브젝트 상태를 동일하거나 다른 컴퓨터 환경에 저장(이를테면 파일이나 메모리 버퍼에서, 또는 네트워크 연결 링크 간 전송)하고 나중에 재구성할 수 있는 포맷으로 변환하는 과정이다.[1] 반대로, 일련의 바이트로부터 데이터 구조를 추출하는 일은 역직렬화 또는 디시리얼라이제이션(deserialization)이라고 한다. - 위키백과
컴퓨터 과학 전반에서 쓰이는 serialize 의 의미는 어떤 데이터를 다른 곳에서도 사용할 수 있게끔 데이터 포맷을 변환하는 과정이다.
즉 serializer 의 역할은 서로 다른 환경 간에 데이터 통신을 할 수 있게 하기 위해 데이터 포맷을 바꿔주는 역할이다.

serialization(직렬화) 와 deserialization(역직렬화) 의 개념

drf 에서의 serialize 를 보기 전에 조금 더 일반적인 serialize 와 deserialize 상황을 살펴보자.
serialize 와 deserialize 의 구체적인 과정은 환경,언어,데이터 포맷에 따라 모두 다르지만 관통하는 핵심은 아래와 같다.
데이터 저장소(메모리) 관점에서, 메모리 내부에 데이터를 저장할 때 데이터는 byte string 형식으로 저장되어야 한다.
하지만 우리가 일반적으로 쓰는 데이터는 byte string 형식이 아닌 여러 데이터 포맷으로 이루어진 복잡한 객체(complex object) 이므로 형식을 바꿔주는 과정이 필요하다.
serialization
deserialization
예시 상황
데이터를 메모리에 저장하는 상황
메모리에서 데이터를 꺼내는 상황
데이터 변환
object(객체) → byte string(메모리)
byte string(메모리)→ object(객체)
대표적인 예시로 JSON을 이용한 serialiaztion, deserializtion 이 있다.
아래와 같은 객체가 있다고 쳐보자.
{foo: [1, 4, 7, 10], bar: "baz"}
Python
복사
위 객체를 그대로 메모리에 저장하는 것은 불가하고 byte string 으로 변환을 해야 메모리에 저장이 가능하다.
이 과정이 serialize 이다.
JSON 으로 serialize 할 시 아래의 string 으로 바뀌게 된다.
이 string 을 메모리에 저장하게 된다.
'{"foo":[1,4,7,10],"bar":"baz"}'
Python
복사
( 물론 실제로 저장될 때는 JSON 이 아닌 JSON 형식의 byte string 으로 저장이 된다. )
이제 위의 string 을 deserialize 하게 되면 원래의 객체를 다시 얻을 수 있다.
{foo: [1, 4, 7, 10], bar: "baz"}
Python
복사

drf 에서의 Serializer

drf 에서의 serialize 는 drf 의 객체(쿼리셋, 모델..)파이썬 데이터타입(리스트, 객체..) 로 바꾸는 것을 말하고,
deserialize 는 반대로 파이썬 데이터타입(리스트, 객체..)drf 의 객체(쿼리셋, 모델..) 로 바꾸는 것을 말한다.
Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSONXML or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data. - drf 공식문서
공식문서에 나와있듯이 serialize 는 파이썬 객체를 JSON 형식으로 바꾸는 것이 아니라, JSON 형식으로 바꾸기 쉽게끔 쿼리셋이나 모델 인스턴스 같은 복잡한 객체를 파이썬 데이터타입으로 바꾸는 역할을 한다.
또한 이름은 serializer 이지만 내부적으로 serialize, deserialize 의 역할을 동시에 한다.
밑에서 살펴보겠지만 serializer 인스턴스에 넘겨주는 파라미터에 따라 serializer 또는 deserializer 로 동작하게 된다.

serializer 의 기능

1.
serialization
2.
deserialiaztion
3.
validation
4.
request / response 데이터 핸들링 ( to_internal_value() / to_representation() )
5.
nested serialization
주요 기능들은 위와 같고 더 세세히 파고들면 더 많은 기능들이 있다.
drf 에서 serializer 는 파라미터를 어떻게 주느냐에 따라 직렬화를 수행할 수도 있고, 역직렬화를 수행할 수 도 있다.
serialization : 쿼리셋, 모델 인스턴스 등의 복잡한 데이터를 JSON, XML 등의 컨텐트 타입으로 쉽게 변환 가능한 python datatype으로 변환시켜줌
deserialization : 받은 데이터를 validating 한 후에 parsed data를 complex type으로 다시 변환

1. Serialization (직렬화)

쿼리셋 또는 모델 객체 → dict → json 형식의 byte string 으로 변환
drf 의 serializer 는 쿼리셋 또는 모델 객체 → dict 이 부분만 담당함
dict → json 변환은 drf 의 default Renderer 인 JsonRenderer 에서 맡음
사용 예시 : 클라이언트에서 서버의 데이터를 읽어올 때 직렬화가 사용됨 ( GET method )
# Comment 객체 하나를 가져옴 >>> c0 = Comment.objects.all()[0] # Comment 정보 확인 >>> c0 <Comment: 첫번째 댓글 입니다> >>> type(c0) <class 'blog.models.Comment'> # CommentSerializer() >>> CommentSerializer() CommentSerializer(): id = IntegerField(label='ID', read_only=True) content = CharField(label='CONTENT', style={'base_template': 'textarea.html'}) create_dt = DateTimeField(label='CREATE DT', read_only=True) update_dt = DateTimeField(label='UPDATE DT', read_only=True) post = PrimaryKeyRelatedField(allow_null=True, queryset=Post.objects.all(), required=False) # CommentSerializer 의 instance 인자로 c0 를 넘겨줌 >>> sr = CommentSerializer(instance=c0) # sr.data 를 찍어보면 dict 로 변환됐음을 확인할 수 있음 >>> sr.data {'id': 1, 'content': '첫번째 댓글 입니다.', 'create_dt': '2021-07-28T07:06:54.974180', 'update_dt': '2021-07-28T07:06:54.974180', 'post': 3} # 순수한 dict 는 아니고 serializer 에 의해 래핑된 dict 임 >>> type(sr.data) <class 'rest_framework.utils.serializer_helpers.ReturnDict'> # JSONRenderer import >>> from rest_framework.renderers import JSONRenderer # dict 를 JSON 으로 변환 >>> json0 = JSONRenderer().render(sr.data) >>> json0 b'{"id":1,"content":"\xec\xb2\xab\xeb\xb2\x88\xec\xa7\xb8 \xeb\x8c\x93\xea\xb8\x80 \xec\x9e\x85\xeb\x8b\x88\xeb\x8b\xa4.","create_dt":"2021-07-28T07:06:54.974180","update_dt":"2021-07-28T07:06:54.974180","post":3}' # bytes 타입인 것을 확인할 수 있음 >>> type(json0) <class 'bytes'>
Python
복사
모델(instance) → serializer(instance=XXX) → dict → json data → response

2. Deserialization (역직렬화)

클라이언트 → 서버 로 데이터를 보내고 DB 에 쓰기 위한 용도로 사용함 ( ex. POST, UPDATE, DELETE, PATCH )
클라이언트(사용자) 로부터 넘어온 데이터는 오류가 있을 수 있다.
따라서 serializer 와는 다르게 클라이언트에서 올바른 데이터가 넘어왔는지 검사하는 valid 과정이 필요하다.
json 형식의 byte string (클라) → python dictionary → 유효성 검사 → DB 저장
drf의 deserializer 는 python dictionary → 유효성 검사 → DB 저장 이 부분만 맡는다.
serializer 의 data 파라미터에 클라로부터 넘어온 데이터(json) 을 넣어주고, is_valid() 를 실행한다.
is_valid() 를 통과하게 되면 통과한 데이터가 validated_data 에 저장되고, save() 를 하면validated_data 가 DB 에 저장된다.
>>> from rest_framework.parsers import JSONParser >>> from io import BytesIO # 1. json형식 byte string 을 dict 로 parsing 함 >>> data0 = JSONParser().parse(BytesIO(json0)) >>> data0 {'id': 1, 'content': '첫번째 댓글 입니다.', 'create_dt': '2021-07-28T07:06:54.974180', 'update_dt': '2021-07-28T07:06:54.974180', 'post': 3} >>> type(data0) <class 'dict'> # 2. 역직렬화 : serializer 의 data 인자에 dict 를 넘겨줌 >>> serializer = CommentSerializer(data=data0) # 3. 유효성 검사 >>> serializer.is_valid() [2023-02-27 17:19:36] [0.002] SELECT "blog_post"."id", "blog_post"."category_id", "blog_post"."title", "blog_post"."description", "blog_post"."image", "blog_post"."content", "blog_post"."create_dt", "blog_post"."update_dt", "blog_post"."like" FROM "blog_post" WHERE "blog_post"."id" = 3 LIMIT 21 True # 4. 유효성 검사 통과한 데이터 확인 >>> serializer.validated_data OrderedDict([('content', '첫번째 댓글 입니다.'), ('post', <Post: 유럽 여행 중 파리 개선문 다녀 왔답니다.>)]) # 5. 객체를 DB 에 저장 >>> instance = Comment(**serializer.validated_data) >>> instance.save() [2023-02-27 17:20:41] [0.002] INSERT INTO "blog_comment" ("post_id", "content", "create_dt", "update_dt") VALUES (3, '첫번째 댓글 입니다.', '2023-02-27 17:20:41.090590', '2023-02-27 17:20:41.090701') RETURNING "blog_comment"."id"
Python
복사
정리
json data 를 dict 로 변환 → serializer(data=XXX)is_valid, validated_dataserializer.save() 혹은 내부적으로 커스텀한 instance 를 save

3. Validation

serializer 에서 정의한 필드말고도 커스텀한 validator 를 만들 수 있다.
아래와 같이 Movie 라는 model 이 있다고 하자.
from django.db import models class Movie(models.Model): title = models.CharField(max_length=128) description = models.TextField(max_length=2048) release_date = models.DateField() rating = models.PositiveSmallIntegerField() us_gross = models.IntegerField(default=0) worldwide_gross = models.IntegerField(default=0) def __str__(self): return f'{self.title}'
Python
복사
1.
특정 필드 validator
validate_[필드명](self, value) 함수는 특정 필드에 대해 유효성 검사를 해준다.
from rest_framework import serializers from examples.models import Movie class MovieSerializer(serializers.ModelSerializer): class Meta: model = Movie fields = '__all__' def validate_rating(self, value): if value < 1 or value > 10: raise serializers.ValidationError('Rating has to be between 1 and 10.') return value
Python
복사
validate_rating : Movie 모델의 rating 필드에 대해 1 이상, 10 이하 인지 유효성 검사
2.
객체 레벨 validator
validate() 함수 내에서 필드에 접근하여 유효성 검사를 할 수 있다.
특히, 모델의 두 필드를 가지고 유효성 검사할 때 유용하다.
from rest_framework import serializers from examples.models import Movie class MovieSerializer(serializers.ModelSerializer): class Meta: model = Movie fields = '__all__' def validate(self, data): if data['us_gross'] > data['worldwide_gross']: raise serializers.ValidationError('worldwide_gross cannot be bigger than us_gross') return data
Python
복사
if data['us_gross'] > data['worldwide_gross']:
인스턴스의 us_grossworldwide_gross 를 비교하는 유효성 검사
3.
validator 함수
serializer 외부에 함수를 생성한 다음, serializer 에서 함수를 불러와서 유효성 검사를 할 수 있다.
def is_rating(value): if value < 1: raise serializers.ValidationError('Value cannot be lower than 1.') elif value > 10: raise serializers.ValidationError('Value cannot be higher than 10')
Python
복사
from rest_framework import serializers from examples.models import Movie class MovieSerializer(serializers.ModelSerializer): rating = IntegerField(validators=[is_rating]) ...
Python
복사

4. request / response 데이터 핸들링 ( to_internal_value() / to_representation() )

1.
to_internal_value() : request 로 들어오는 데이터를 핸들링 (역직렬화 상황)
2.
to_representation() : response 로 나가는 데이터를 핸들링 (직렬화 상황)
Resource 모델 정의
from django.contrib.auth.models import User from django.db import models class Resource(models.Model): title = models.CharField(max_length=256) content = models.TextField() liked_by = models.ManyToManyField(to=User) def __str__(self): return f'{self.title}'
Python
복사
ResourceSerializer 정의
from rest_framework import serializers from examples.models import Resource class ResourceSerializer(serializers.ModelSerializer): class Meta: model = Resource fields = '__all__'
Python
복사
to_internal_value()
ResourceSerializer 의 유효성 검사를 통과하기 위해서는 정확히 Resource 모델의 필드만 존재해야 한다.
그런데 때로는 request 에 다른 정보가 같이 올 수도 있는데, 이런 경우에는 Serializer 에서 유효성 검사를 통과하지 못하게 된다.
{ "info": { "extra": "data", ... }, "resource": { "id": 1, "title": "C++ with examples", "content": "This is the resource's content.", "liked_by": [ 2, 3 ], "likes": 2 } }
JSON
복사
info, extra 등 다른 데이터들이 같이 넘어오는 상황
이런 상황에서 to_internal_value() 를 이용해 serializer 에 들어가는 데이터를 핸들링할 수 있다.
from rest_framework import serializers from examples.models import Resource class ResourceSerializer(serializers.ModelSerializer): class Meta: model = Resource fields = '__all__' def to_internal_value(self, data): resource_data = data['resource'] return super().to_internal_value(resource_data)
Python
복사
to_representation()
to_internal_value() 가 역직렬화 상황에서 사용됐다면, to_representation() 는 직렬화 상황에서 쓰인다.
serializer.data 를 response 로 보내게 되면, serializer 에 정의된 필드대로만 response 가 나가게 된다.
from rest_framework import serializers from examples.models import Resource class ResourceSerializer(serializers.ModelSerializer): class Meta: model = Resource fields = '__all__' def to_representation(self, instance): representation = super().to_representation(instance) representation['likes'] = instance.liked_by.count() return representation
Python
복사
직렬화되는 데이터에 likes 라는 key 를 추가함
{ "id": 1, "title": "C++ with examples", "content": "This is the resource's content.", "liked_by": [ 2, 3 ], "likes": 2 }
JSON
복사
to_representation() 에 의해 response 에 likes 라는 필드가 추가되었다.

5. nested serialization

serializer 안에 serializer 를 사용할 수 있다.
참조하는 테이블의 값을 때 유용하다.
정참조 : 자식테이블에서 부모테이블을 참조 ( FK 가 있는 곳에서 없는 곳으로 )
역참조 : 부모테이블에서 자식테이블을 참조 ( FK 가 없는 곳에서 있는 곳으로 )
방법은 2가지 방법이 있다.
1.
serializer 안에서 다른 serializer를 명시적으로 정의
2.
depth 사용
방법1 ) serializer 안에서 다른 serializer를 명시적으로 정의
from django.contrib.auth.models import User from django.db import models class Comment(models.Model): author = models.ForeignKey(to=User, on_delete=models.CASCADE) datetime = models.DateTimeField(auto_now_add=True) content = models.TextField()
Python
복사
User : 부모테이블
Comment : 자식테이블
from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['id', 'username'] class CommentSerializer(serializers.ModelSerializer): author = UserSerializer() class Meta: model = Comment fields = '__all__'
Python
복사
CommentSerializer 안에서 UserSerializer 를 사용
Comment 테이블에서 User 테이블을 참조 (정참조)
{ "id": 1, "author": { "id": 1, "username": "admin" }, "datetime": "2021-03-19T21:51:44.775609Z", "content": "This is an interesting message." }
JSON
복사
response 는 이런 형태로 나가게 된다.
방법2 ) depth 사용
from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['id', 'username'] class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment fields = '__all__' depth = 1
Python
복사
CommentSerializer 에서 따로 필드를 선언 안 하고, depth 를 줘도 방법1 과 같은 결과를 얻는다.
참조하고 싶은 모델의 깊이에 따라 depth 값을 조정해주면 된다.
주의 nested serializer 를 그냥 사용할 시, N+1 문제가 발생할 수 있으니 주의해야 한다. 공식문서에서도 이 내용을 언급하고 있다.
django orm 은 lazy loading 으로 동작하기 때문에 N+1 문제를 방지하려면 selected_related( join 효과) 또는 prefetch_related ( 추가쿼리 ) 를 사용해야 한다. 예제)
class Author(models.Model): name = models.CharField(max_length=50) class Book(models.Model): title = models.CharField(max_length=100) author = models.ForeignKey(Author, on_delete=models.CASCADE) published_date = models.DateField()
Python
복사
위와 같이 Author 와 Book 이라는 두 개의 모델이 있다고 가정해보자.
그리고 아래와 같은 API 응답을 내고자 한다.
[ { "id": 1, "title": "The Great Gatsby", "published_date": "2021-08-01", "author": { "id": 1, "name": "F. Scott Fitzgerald" } }, { "id": 2, "title": "To Kill a Mockingbird", "published_date": "2021-08-02", "author": { "id": 2, "name": "Harper Lee" } } ]
JSON
복사
위와 같은 API 응답을 내기 위해서는 BookSerializer 안에 AuthorSerializer 를 넣어야 한다.
class BookSerializer(serializers.ModelSerializer): author = AuthorSerializer() class Meta: model = Book fields = '__all__'
Python
복사
books = Book.objects.all() serializer = BookSerializer(books, many=True)
Python
복사
하지만 이렇게 작성할 경우, SELECT * FROM author 와 같은 쿼리가 Book 의 개수만큼 실행되기 때문에 N+1 문제가 발생한다.
이 문제를 해결하기 위해서는 select_related() 를 사용하면 된다.
books = Book.objects.select_related('author').all() serializer = BookSerializer(books, many=True)
Python
복사
select_related() 를 함께 사용하면, SQL 쿼리를 하나의 JOIN 쿼리로 합쳐서 실행하기 때문에, N+1 문제를 해결할 수 있다.
만약 prefetch_related() 를 사용하면 메인쿼리와 WHERE..IN 쿼리, 총 두 개로 해결이 가능하다.
books = Book.objects.prefetch_related('author').all() serializer = BookSerializer(books, many=True)
Python
복사
다만 위와 같은 정참조 상황( FK 가 있는 곳에서 없는 곳으로 ) 에서는 prefetch_related() 보다 select_related() 를 사용하는 것이 더 효율적이다.
반대로 역참조 상황에서는 prefetch_related() 를 사용하는 것이 좋다.
Reference