[데이터 엔지니어링 데브코스 2기] TIL-4주차-파트04 장고를 활용한 API서버 만들기(Serializer, HTTP Methods, Mixin, generics)

이재호·2023년 11월 1일
0

1. Serializer와 Deserializer

  • Serializer : 모델 인스턴스나 QuerySet과 같은 데이터를 JSON 형식의 파일로 변환하는 작업입니다.
    Model -> JSON
  • Descrializer : Serializer의 반대 개념으로, JSON 데이터를 정해진 포맷에 맞춰 Model로 변환하는 작업입니다.
    JSON -> Model

Serializer를 코드로 구현하는 방법은 다음과 같습니다. 이전의 프로젝트 환경에서,

  1. python manage.py startapp polls_api 명령어를 입력하여 기능을 정의하기 위한 api 앱을 만듭니다.
  2. polls_api/serializers.py 파일을 생성 후, 다음과 같이 작성합니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question

class QuestionSerializer(serializers.Serializer):
    # 필드의 특성에 따라 read_only와 max_length 등의 옵션 입력.
    id = serializers.IntegerField(read_only=True)
    question_text = serializers.CharField(max_length=200)
    pub_date = serializers.DateTimeField(read_only=True)

    # DB에 새로운 객체 생성
    def create(self, validated_data):
        # validated_data를 기반으로한 새로운 Question 생성
        return Question.objects.create(**validated_data)

    # 해당 객체의 question_text를 수정
    def update(self, instance, validated_data):
        # .get(필드명, 해당 필드명이 존재하지 않을 시의 대체재)
        instance.question_text = validated_data.get('question_text', instance.question_text)
        instance.save()
        return instance

만약 rest_framework가 없다면, pip install djangorestframework 명령어로 설치를 해 줘야 합니다.




2. Django Shell 실습

위의 serializer 코드를 가지고 Django shell에서 코드를 실행해 봅니다.

  1. polls_api/serializers.py 파일을 다음과 같이 수정합니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question

class QuestionSerializer(serializers.Serializer):
    # 필드의 특성에 따라 read_only와 max_length 등의 옵션 입력.
    id = serializers.IntegerField(read_only=True)
    question_text = serializers.CharField(max_length=200)
    pub_date = serializers.DateTimeField(read_only=True)

    # DB에 새로운 객체 생성.
    def create(self, validated_data):
        # validated_data를 기반으로한 새로운 Question 생성
        return Question.objects.create(**validated_data)

    # 해당 객체의 question_text를 수정
    def update(self, instance, validated_data):
        # .get(필드명, 해당 필드명이 존재하지 않을 시의 대체재)
        instance.question_text = validated_data.get('question_text', instance.question_text) + '[시리얼라이저에서 업데이트]'
        instance.save()
        return instance
  1. Django Shell에서 코드를 검사해 봅니다.
# 모델을 임포트합니다.
>>> from polls.models import *

# Question 모델 q를 지정합니다.
>>> q = Question.objects.first()
>>> q
<Question:  제목: 휴가를 보내고 싶은 장소는?, 날짜: 2023-10-30 06:32:04+00:00>

# 시리얼라이저를 임포트합니다.
>>> from polls_api.serializers import *

# Question 모델 q에 대한 시리얼라이저를 생성합니다.
>>> serializer = QuestionSerializer(q)
>>> serializer.data
{'id': 1, 'question_text': '휴가를 보내고 싶은 장소는?', 'pub_date': '2023-10-30T06:32:04Z'}

# JSON파일로 변환하기 위해 rest_framework.renderers에서 JSONRenderer을 임포트합니다. 
>>> from rest_framework.renderers import JSONRenderer

# serializer의 data를 JSON으로 렌더링합니다.
>>> json_str = JSONRenderer().render(serializer.data)
# JSON 형식의 문자열을 확인할 수가 있습니다.
>>> json_str
b'{"id":1,"question_text":"\xed\x9c\xb4\xea\xb0\x80\xeb\xa5\xbc \xeb\xb3\xb4\xeb\x82\xb4\xea\xb3\xa0 \xec\x8b\xb6\xec\x9d\x80 \xec\x9e\xa5\xec\x86\x8c\xeb\x8a\x94?","pub_date":"2023-10-30T06:32:04Z"}'

# json.loads()는 JSON 형식의 데이터를 딕셔너리 형태로 Deserialize합니다.
>>> import json
>>> data = json.loads(json_str)
>>> data
{'id': 1, 'question_text': '휴가를 보내고 싶은 장소는?', 'pub_date': '2023-10-30T06:32:04Z'}

# 위의 JSON 데이터를 시리얼라이저하는 시리얼라이저 객체를 생성합니다.
>>> serializer = QuestionSerializer(data=data)

# 해당 데이터가 valid한지 검사합니다.
>>> serializer.is_valid()
True

# 시리얼라이저의 validated_data를 확인합니다.
>>> serializer.validated_data
OrderedDict([('question_text', '휴가를 보내고 싶은 장소는?')])

# 시리얼라이저의 save 메서드를 호출합니다.
# 해당 시리얼라이저의 경우, 모델 인스턴스 없이 데이터만 존재하므로
# save() 메서드를 호출하면 create() 메서드가 호출될 것입니다.
>>> new_question = serializer.save()
>>> new_question
<Question: New!!! 제목: 휴가를 보내고 싶은 장소는?, 날짜: 2023-11-01 04:34:09.530715+00:00>
>>> new_question.id
10

# Question 모델 객체들을 보면, 마지막에 new_question 객체가 추가된 것을 확인할 수가 있습니다.
>>> Question.objects.all()
< ... , <Question: New!!! 제목: 휴가를 보내고 싶은 장소는?, 날짜: 2023-11-01 04:34:09.530715+00:00>]>

# 인스턴스와 데이터가 주어진 시리얼라이저를 확인해 봅니다.
>>> data = {'question_text' : '제목 수정'}
>>> serializer = QuestionSerializer(new_question, data=data)
# **주의 사항**
# validated_data 및 save()를 호출하기 전에 is_valid()를 호출해야 합니다.
>>> serializer.validated_data
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "C:\Users\lb948\Desktop\Programmers\django\lib\site-packages\rest_framework\serializers.py", line 271, in validated_data
    raise AssertionError(msg)
AssertionError: You must call `.is_valid()` before accessing `.validated_data`.
>>> serializer.is_valid()
True

# 인스턴스와 데이터가 주어진 시리얼라이저의 save()를 호출합니다.
# 해당 시리얼라이저의 경우, 인스턴스가 주어졌기에 update 메서드가 호출될 것입니다.
>>> serializer.save()
<Question: New!!! 제목: 제목 수정[시리얼라이저에서 업데이트], 날짜: 2023-11-01 04:34:09.530715+00:00>

# not valid한 데이터에 대해서도 테스트해 봅니다.
>>> long_text = 'abcd'*300
>>> data = {'question_text' : long_text}
>>> serializer = QuestionSerializer(data=data)
>>> serializer.is_valid()
False
# 에러에 대한 로그를 확인합니다. 
# question_text 필드의 길이가 200이 넘어가서 에러가 발생한 것을 확인할 수가 있습니다.
>>> serializer.errors
{'question_text': [ErrorDetail(string='Ensure this field has no more than 200 characters.', code='max_length')]}
# valid하지 않기 때문에 validated_data에는 아무 것도 들어 있지 않습니다.
>>> serializer.validated_data
{}

3. Model Serializer

위의 serializer처럼 구현해도 되지만, ModelSerializer를 활용하면 더욱더 간편하게 구현할 수가 있습니다.

  1. polls_api/serializers.py 파일을 다음과 같이 수정합니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question

# ModelSerializer를 상속받는 경우, 필드 및 메서드가 자동으로 지정됨.
class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date']
  1. Django Shell에서 해당 코드를 확인해 봅니다.
>>> from polls_api.serializers import *
# 시리얼라이저를 출력해 보니, 필드들에 대한 옵션이 자동으로 지정된 것을 확인할 수가 있습니다.
>>> print(QuestionSerializer())
QuestionSerializer():
    id = IntegerField(read_only=True)
    question_text = CharField(max_length=200)
    pub_date = DateTimeField(read_only=True)

# 새로운 시리얼라이저를 생성하고, save()를 호출해 봅니다.
>>> serializer = QuestionSerializer(data={'question_text' : '모델 시리얼라이저로 만들어 봅니다.'})
>>> serializer.is_valid()
True
# create 메서드를 지정하지 않아도 자동으로 create된 것을 확인할 수가 있습니다.
>>> serializer.save()
<Question: New!!! 제목: 모델 시리얼라이저로 만들어 봅니다., 날짜: 2023-11-01 05:01:04.897276+00:00>



4. Http Methods

Http Methods에 대한 정보를 다음 웹 사이트에서 확인할 수가 있습니다.
https://developer.mozilla.org/ko/docs/Web/HTTP/Methods

  • POST (CREATE) : 특정 리소스에 엔티티를 제출할 때(생성할 때) 쓰입니다.
  • GET (READ) : 특정 리소스의 표시를 요청합니다.
  • PUT (UPDATE) : 목적 리소스의 내용을 수정할 때 쓰입니다.
  • DELETE (DELETE) : 특정 리소스를 삭제합니다.



5. GET 메서드

장고에서 GET 메서드에 대한 코드는 다음과 같습니다.

  1. polls_api/views.py 파일을 다음과 같이 수정합니다.
from polls.models import Question
from polls_api.serializers import QuestionSerializer
from rest_framework.response import Response
from rest_framework.decorators import api_view

# Create your views here.

# # api_view 데코레이터의 ()가 비어있는 경우, question_list가 get 요청을 처리한다는 의미.
@api_view()
def question_list(request):
    questions = Question.objects.all()
    serializer = QuestionSerializer(questions, many = True) # many옵션으로 여러 개 등록 가능.
    return Response(serializer.data)
  1. polls_api/urls.py 파일을 생성 후 다음과 같이 작성합니다.
from django.urls import path
from .views import *

urlpatterns = [
    path('question/', question_list, name='question-list'),
]
  1. mysite(프로젝트 폴더명)/urls.py 파일을 다음과 같이 수정합니다.
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('polls/', include('polls.urls')),
    path('rest/', include('polls_api.urls')),
]
  1. http://127.0.0.1:8000/rest/question/ 에서 결과를 확인합니다.

    강의 영상과 다른 화면이 나옵니다(TemplateDoesNotExist). 강의에서는 장고의 rest 프레임워크가 get한 데이터들을 예쁘게 보여 주고 있지만, 저는 다르게 화면이 나옵니다. 원인을 찾아 보니, settings.py에서 INSTALLED_APPS에 'rest_framework'가 없었습니다. 따라서 이를 추가해 주었습니다.
  • mysite/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'polls.apps.PollsConfig',
    # 추가된 앱
    'rest_framework',
]

추가 후 이미지)




6. POST 메서드

먼저 Status Code에 대해서 작성하겠습니다.

  • 200번대 : 정상.
    • 200 : OK
    • 201 : CREATED
  • 400번대 : 사용자의 잘못된 요청.
    • 400 : BAD REQUEST
    • 404 : Page Not Found
  • 500번대 : 서버 내부의 오류.

다음으로는 POST 메서드를 구현한 코드입니다.

  1. polls_api/views.py를 다음과 같이 수정합니다.
from polls.models import Question
from polls_api.serializers import QuestionSerializer
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status

# Create your views here.

# GET과 POST에 대해서 api_view 데코레이터 작성.
@api_view(['GET', 'POST'])
def question_list(request):
    if request.method == 'GET':
        questions = Question.objects.all()
        serializer = QuestionSerializer(questions, many = True) # many옵션으로 여러 개 등록 가능.
        return Response(serializer.data)
    
    if request.method == 'POST':
        serializer = QuestionSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save() # 인스턴스가 주어지지 않으므로 create() 메서드가 호출됨.
            # 잘된 응답에 대해서 status 201 code 리턴.
            # status code가 200번대: OK
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            # 잘못된 응답에 대해서 status 400 code 리턴.
            # status code가 400번대: 사용자의 잘못된 요청
            # 500번대: 서버 내부의 오류
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  1. http://127.0.0.1:8000/rest/question/ 에서 OPTIONS를 클릭하여 데이터를 등록(POST)합니다.

  2. 세 가지의 경우에 대해서 테스트를 하였습니다.

    1. 정상적인 입력
    2. 비정상적인 입력(필드 조건 불만족, 빈 내용 post)



7. PUT과 DELETE 메서드

위와 유사한 방식으로 PUT과 DELETE 메서드를 구현할 수 있습니다. 절차는 다음과 같습니다.

  1. polls_api/views.py 파일을 다음과 같이 수정합니다.
from django.shortcuts import render, get_object_or_404
from polls.models import Question
from polls_api.serializers import QuestionSerializer
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status

# Create your views here.

# GET과 POST에 대해서 api_view 데코레이터 작성.
@api_view(['GET', 'POST'])
def question_list(request):
    if request.method == 'GET':
        questions = Question.objects.all()
        serializer = QuestionSerializer(questions, many = True) # many옵션으로 여러 개 등록 가능.
        return Response(serializer.data)
    
    if request.method == 'POST':
        serializer = QuestionSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save() # 인스턴스가 주어지지 않으므로 create() 메서드가 호출됨.
            # 잘된 응답에 대해서 status 201 code 리턴.
            # status code가 200번대: OK
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            # 잘못된 응답에 대해서 status 400 code 리턴.
            # status code가 400번대: 사용자의 잘못된 요청
            # 500번대: 서버 내부의 오류
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        

# 하나의 Question에 대한 뷰
@api_view(['GET', 'PUT', 'DELETE'])
def question_detail(request, id):
    question = get_object_or_404(Question, pk=id)

    if request.method == 'GET':
        serializer = QuestionSerializer(question)
        return Response(serializer.data)
    
    if request.method == 'PUT':
        serializer = QuestionSerializer(question, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        
    if request.method == 'DELETE':
        question.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
  1. polls_api/urls.py 파일에 경로를 추가해 줍니다.
from django.urls import path
from .views import *

urlpatterns = [
    path('question/', question_list, name='question-list'),
    path('question/<int:id>/', question_detail, name='question_detail'),
]
  1. 브라우저에서 특정 question의 id에 대해서 링크를 접속해 봅니다.

    해당 Question에 대해서 PUT과 DELETE 기능이 추가된 것을 확인할 수가 있습니다.



8. Class 뷰

위와 같이 데코레이터를 사용하여 뷰를 지정할 수도 있지만, Class로 지정할 수도 있습니다. Class로 지정할 경우, 코드가 더욱 간편해진다는 장점이 있습니다. 코드는 다음과 같습니다.

  1. polls_api/views.py
from django.shortcuts import render, get_object_or_404
from polls.models import Question
from polls_api.serializers import QuestionSerializer
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status
from rest_framework.views import APIView

# APIView를 상속받는 class 뷰 생성
class QuestionList(APIView):
    def get(self, request):
        questions = Question.objects.all()
        serializer = QuestionSerializer(questions, many = True) # many옵션으로 여러 개 등록 가능.
        return Response(serializer.data)

    def post(self, request):
        serializer = QuestionSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save() # 인스턴스가 주어지지 않으므로 create() 메서드가 호출됨.
            # 잘된 응답에 대해서 status 201 code 리턴.
            # status code가 200번대: OK
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            # 잘못된 응답에 대해서 status 400 code 리턴.
            # status code가 400번대: 사용자의 잘못된 요청
            # 500번대: 서버 내부의 오류
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)        

class QuestionDetail(APIView):
    def get(self, request, id):
        question = get_object_or_404(Question, pk=id)
        serializer = QuestionSerializer(question)
        return Response(serializer.data)

    def put(self, request, id):
        question = get_object_or_404(Question, pk=id)
        serializer = QuestionSerializer(question, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, id):
        question = get_object_or_404(Question, pk=id)
        question.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
  1. polls_api/urls.py
from django.urls import path
from .views import *

urlpatterns = [
    path('question/', QuestionList.as_view, name='question-list'),
    path('question/<int:id>/', QuestionDetail.as_view, name='question_detail'),
]

데코레이터로 지정했을 때와 결과는 동일합니다. 하지만 클래스로 지정할 경우, 더욱 코드가 간편해진 것을 확인할 수가 있습니다.


9. Mixin(뷰) 클래스

장고 프레임워크에서 제공하는 클래스로 편리한 기능을 가지고 있습니다.
위에서 보았던 클래스 뷰를 더욱더 간단하게 작성할 수가 있습니다. 코드는 다음과 같습니다.

  • polls_api/views.py
from django.shortcuts import render, get_object_or_404
from polls.models import Question
from polls_api.serializers import QuestionSerializer
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status, mixins, generics
from rest_framework.views import APIView

# Mixin과 Generic 뷰를 상속받는 class 뷰 생성
# ListModelMixin : GET / CreateModelMixin : POST
class QuestionList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)
    
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

# Mixin과 Generic 뷰를 상속받는 class 뷰 생성
# RetrieveModelMixin : GET / UpdateModelMixin : UPDATE /
# DestroyModelMixin : DELETE
class QuestionDetail(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer
    
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)
  • polls_api/urls.py
from django.urls import path
from .views import *

urlpatterns = [
    path('question/', QuestionList.as_view, name='question-list'),
    # generics 뷰는 pk를 받아서 queryset을 구성함.
    path('question/<int:pk>/', QuestionDetail.as_view, name='question_detail'),
]

10. Generic API 뷰

위의 코드를 더욱더 간단하게 표현할 수가 있습니다.
이는 generics 클래스가 위의 기능을 이미 구현해 놓았기 때문에 가능한 것입니다.

코드는 다음과 같습니다.

  • polls_api/views.py
from polls.models import Question
from polls_api.serializers import QuestionSerializer
from rest_framework import generics

# generics 클래스의 ListCreateAPIView 상속
# get, post 등의 기능이 구현되어 있음
class QuestionList(generics.ListCreateAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

# generics 클래스의 RetrieveUpdateDestroyAPIView 상속
# get, put, delete 등의 기능이 구현되어 있음.
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

매우 간편해진 것을 확인할 수가 있습니다.

profile
천천히, 그리고 꾸준히.

0개의 댓글