snippet 인스턴스들과 json 과 같은 representations들과 직렬화/역직렬화할 수 있어야 한다.
from rest_framework import serializers
from .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")
def create(self, validated_data):
"""
Create and return a new 'Snippet' instance, given the validated data.
"""
return Snippet.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing 'Snippet' instance, given the 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
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()
가 호출될 때, 실행되는 메서드다.처음 DRF를 배울 땐, serializers.py에 비즈니스 로직을 넣고 save 혹은 **validated_data
를 멋있게 활용해 효과적으로 인스턴스를 저장하고 싶은 마음이 앞섰다.
하지만 생각이 많이 바꼈다.
DRF가 사실상 Django 프레임워크의 필수 패키지가 돼 버린 것은 맞지만,
그렇다고 DRF가 제공하는 모든 기능이 옳다고 볼 수는 없다고 생각한다.
단일책임원칙(SRP)은 코드 수준에서만 적용되는 개념은 아니다. 절대.
모듈 수준에서도 똑같이 적용된다.
사람들은 단일책임원칙의 정의가 함수 혹은 클래스는 단 하나의 일(책임)만 해야 한다 라고 알고 있다.
2% 부족한 정의라 생각해왔다.
해당 함수나 모듈의 변경은 오로지, 단 하나의 액터(actor)에 의해서만 가능해야 한다는 것이다.
serializers.py에 비즈니스 로직이 섞여 있는 그림을 상상해 보자.
디자이너와 프론트엔드 측에서 리턴 형식을 변경해달라고 요청이 왔다.
그러면 나는 serialziers.py
를 수정하게 될 것이다.
이번엔 애자일팀 혹은 대표의 요청에 의해 비즈니스 로직 수정에 대한 요청이 왔다. 이를테면 환불 정책의 완화일 수도 있고, 무궁무진할 것이다. 그런데 문제는 이 상황에서도 나는 serializers.py
를 수정해야 한다는 것이다.
하나의 모듈에 두 명의 actor가 존재하게 된 상황이다.
테스트 코드를 작성할 시점에 의아함을 느끼게 된다.
"비즈니스 로직을 테스트 하는데, 왜 serializer를 임포트해야 하지?"
그래서 나는 서비스 계층(services.py or logic.py)을 앱별로 생성해야 한다고 생각한다.
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
snippet = Snippet(code='foo = "bar"\n')
snippet.save()
snippet = Snippet(code='print("hello, world")\n')
snippet.save()
>>> serializer = SnippetSerializer(snippet)
>>> serializer.data
{
'id': 2,
'title': '',
'code': 'print("hello, world")\n',
'linenos': False,
'language': 'python',
'style': 'friendly'
}
snippet 인스턴스 중 하나를 직렬화하는 모습을 보고 있다.
Django의 ORM으로 2차원 평면에 펼쳐진 row하나를 인스턴스로 가져왔다.
그리고 파이썬 내장 데이터 타입으로 직렬화했다.
테이블
id | title | code | linenos | language | style |
---|---|---|---|---|---|
1 | foo = "bar"\n | False | python | friendly | |
2 | print("hello, world")\n | False | python | friendly |
인스턴스
>>> snippet
<Snippet: Snippet object (2)>
직렬화
>>> SnippetSerializer(snippet).data
{
'id': 2,
'title': '',
'code': 'print("hello, world")\n',
'linenos': False,
'language': 'python',
'style': 'friendly'
}
API를 Application Programming Interface 즉, 인터페이스로써 일관된 프로토콜이 있기 마련이고, 그 표현방식으로 json을 사용한다.
>>> from rest_framework.renderers import JSONRenderer
>>> from rest_framework.parsers import JSONParser
>>> content = JSONRenderer().render(serializer.data)
>>> content
b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}'
>>> import io
>>> stream = io.BytesIO(content)
>>> data = JSONParser().parse(stream)
>>> data
{
'id': 2,
'title': '',
'code': 'print("hello, world")\n',
'linenos': False,
'language': 'python',
'style': 'friendly'
}
>>> type(data)
<class 'dict'>
>>> serializer = SnippetSerializer(data=data)
>>> serializer.is_valid()
True
>>> serializer.validated_data
OrderedDict([('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
>>> serializer.save()
<Snippet: Snippet object (3)>
단일 인스턴스 뿐만 아니라 쿼리셋에도 직렬화를 적용할 수 있다.
>>> 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')])]
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = [
"id",
"title",
"code",
"linenos",
"language",
"style",
]
모델 시리얼라이저로 많은 추상화가 발생했다.
ModelSerializer
를 사용함으로써 필드 수준의 정의가 불필요해졌다.
기억할 건 두 가지다.
1) 직접 필드를 정의할 필요가 없이 모델이 가진 필드 정의를 가져온다.
2) create, update 메서드가 디폴트로 제공된다.
DRF 말고 장고의 내장 기능들을 이용하여 API를 만들어 보자.
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):
"""
- List all code snippets
- Create a new snippet"""
if request.method == "GET":
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return JsonResponse(serializer.data, safe=False)
elif request.method == "POST":
data = JSONParser().parse(request)
serializer = SnippetSerializer(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):
"""
- Retrieve
- Update
- Delete
"""
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)
아직까지는 body데이터의 정보를 힘겹게 얻어내고 있다.
data = JSONParser().parse(request)
serializer = SnippetSerializer(snippet, data=data)
또한,
JsonResponse, HttpResponse 역시 DRF이 아닌 장고 스타일이다.
튜토리얼을 계속 진행해보며 어떻게 DRF를 조금씩 적용시킬지 지켜보는 재매가 있겠다.