[CEOS] 3주차 미션: DRF1 - Serializer, API View & Filter

HAEN·2023년 5월 4일
0

시험 기간 이슈로 인해...! 오랜만에 글을 쓴다 하하
3주차 미션은 Serializer, API View & Filter를 알아보고 DRF를 본격적으로 사용해보는 것이었다
이번에는 코드를 짜고 API 테스트하는 것이 위주였기 때문에 개념 정리 및 회고를 기록해보겠다


1. DRF란?

(1) 의미

Django에서 웹 API를 RESTful하게 만들 수 있도록 도와주는 라이브러리를 Django REST Framework, 줄여서 DRF라고 한다

(2) 기능

1. 직렬화(Serialization)
DRF는 Django의 모델 데이터를 쉽게 JSON, XML 등의 형식으로 변환할 수 있는 직렬화 기능을 제공한다. 이를 통해 API를 통해 데이터를 주고받을 때, 효율적이고 일관된 방식으로 데이터를 처리할 수 있다.

2. 인증(Authentication)
DRF는 다양한 인증 기능을 제공한다. Django 기본 인증 시스템을 사용하거나, OAuth, JWT 등의 다양한 인증 방식을 쉽게 구현할 수 있다.

3. 권한 설정(Permission)
DRF는 API에 대한 권한을 설정하는 기능을 제공한다. Django의 기본 권한 시스템을 사용하거나, 필요에 따라 커스텀 권한을 설정할 수 있다.

4. ViewSet과 Router
DRF는 ViewSet과 Router를 통해 CRUD(Create, Read, Update, Delete) 작업을 쉽게 구현할 수 있다. ViewSet은 Django의 View와 유사하게 동작하며, Router는 URL 패턴을 자동으로 생성해준다.

5. API 문서화
DRF는 API 문서화를 쉽게 구현할 수 있는 기능을 제공한다. API View의 설명, 파라미터, 응답 등을 자동으로 문서화해준다.

(3) 적용

pip install djangorestframework 명령어를 통해 DRF를 설치한 후
settings.py의 INSTALLED_APPS에 rest_framework를 추가한다

INSTALLED_APPS = [
	...
	'rest_framework',
]


2. Serializer

DRF의 Serializer는 클라이언트의와의 요청에 대해 데이터를 JSON, XML 등으로 변환하는 기능을 제공한다.
또한 Deserialization도 제공하여, 클라이언트를 통해 받은 데이터를 검증(validation)하여 복잡한 데이터로 쉽게 변환할 수 있다.

지난번 과제에서 모델들을 다 짜놓았기 때문에 ModelSerializer를 사용했다

from rest_framework import serializers


class PostListSerializer(serializers.ModelSerializer):
    user_nickname = serializers.SerializerMethodField()
    comments_count = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = ['user', 'user_nickname', 'title', 'content', 'like_number', 'comments_count', 'created_at']


    def get_user_nickname(self, obj):
        return obj.user.nickname

    def get_comments_count(self, obj):
        reply_count = 0

        for comment in obj.comments.all():
            reply_count += comment.comment_replies.count()

        return obj.comments.count() + reply_count

Serializer 클래스 내에 Meta 클래스를 선언하여 modelfields를 명시해준다 그러면 이 필드들이 JSON 데이터로 변환되어 클라이언트와의 통신에 사용된다

이때 fields를 '\__all\__'로 설정하면 모델의 모든 필드를 가져오며,
exclude를 통해 제외할 필드를 설정할 수도 있다

기존 모델에 없는 필드는 get_필드명 함수를 만들어 필드를 추가해 직접 데이터를 반환할 수도 있다
이때는 serializer.SerilizerMethodField()를 사용한다


두 모델이 relationship을 가지고 있을 때는 nested serializer를 사용한다

class TrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = Track
        fields = ['order', 'title', 'duration']


class AlbumSerializer(serializers.ModelSerializer):
    tracks = TrackSerializer(many=True, read_only=True)

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']

AlbumSerializertracks를 추가하여 track 모델을 참조할 수 있다
이때 리스트를 참조한다면 many=True 옵션을 설정해준다



3. API View

앞서 만든 Serializer를 사용하기 위해서는 view를 만들어야 한다
Django에서의 view 동작 방식은 FBV, CBV 두 가지가 있다

(1) FBV (Function-Based View)

# views.py
...

@csrf_exempt
def snippet_list(request):
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)

위와 같이 함수를 통해 HTTP 메소드를 구분하는 방식을 FBV라고 한다

  • 특징
    • 직접 함수를 작성하여 request를 처리
    • 다수의 request 메소드를 사용할 경우 if 조건문으로 구분하여 처리
  • 장점 : 함수로 정의하기에 직관적이고 구현하기 쉬움
  • 단점 : 확장성과 재사용성이 낮음

(2) CBV (Class-Based View)

#views.py

from django.http import HttpResponse
from django.views import View

class MyView(View):
    def get(self, request):
        # <view logic>
        return HttpResponse('result')

위와 같이 class 내부의 함수를 통해 HTTP 메소드를 구분하는 방식을 CBV라고 한다

CBV는 FBV의 단점을 보완하여 확장성과 재사용성이 좋다
하지만 CBV가 FBV를 완벽하게 대체하는 것은 아니므로, 둘 중 어떤 것이 정답이라는 것은 없으므로 상황에 맞춰 사용하면 된다고 한다!


(3) DRF API View

Django Rest Framework 에서 만든 API View이며
FBV, CBV 둘 다 제공한다

# FBV

from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view(['GET'])
def group_list(request):
	if request.method == 'GET':
		return Response({"message":"group list"})

# CBV

from .serializers import UserSerializer, GroupSerializer
from rest_framework.views import APIView
...

class GroupList(APIView):	# APIView를 상속받아 사용한다
		def get(self, request, format=None):
				...

		def post(self, request, format=None):
				...

class GroupDetail(APIView):
		def get(self, request, pk, format=None):
				...

		def delete(self, request, pk, format=None):
				...

이번 미션은 DRF APIView의 CBV 방식을 사용해서 구현하는 것이 목표였다
확실히 FBV 방식이 더 직관적이고 쉬워보이긴 한다


class PostList(APIView):
	def get(self, request, board_id, format=None):
		post_list = Post.objects.filter(board__id=board_id)
		serializer = PostListSerializer(post_list, many=True)
		return Response(serializer.data, status=200)

	def post(self, request, board_id, format=None):
		serializer = PostSerializer(data=request.data)
		if serializer.is_valid():
			serializer.save(board_id=board_id)
			return Response(status=201)
		return Response(serializer.errors, status=400)

위는 내가 구현한 코드이다
게시글 리스트 get 요청을 받으면 filter를 통해 해당하는 포스트들만 반환해준다
게시글 작성 post 요청에서는 serializer를 우선 검증하고, save()를 통해 데이터베이스에 새로운 게시글을 저장한다


view를 사용하기 위해서 urls.py에 url를 등록해준다

urlpatterns = [
    path('<int:board_id>/', views.PostList.as_view()),
    path('<int:board_id>/<int:post_id>/', views.PostDetail.as_view()),
]

as_view()는 말 그대로 이 class를 view로 사용하겠다는 의미이다
클라이언트에서 요청이 오면 url 패턴이 일치하는 view가 해당 요청을 처리한다


(4) ViewSet

DRF에서 지원하는 view의 형태는 단계적이다
APIView는 기존 django의 view를 상속받아 만들어진 것인데,
기존의 간단한 형태의 FBVCBV에서는 하나의 url에 하나의 View만을 매핑했다
하지만 기본적인 post, get 요청 등을 패턴화 및 구조화 시킨 Viewset을 사용하면 코드를 획기적으로 줄일 수가 있다!

  • DRF화:  django view → rest_framework APIView
  • 패턴화: APIView → Generic Views
  • 구조화: Generic Views → Viewset

generics의 ListCreateAPIViewRetrieveUpdateDestroyAPIView 등과 같은 형태의 뷰는 하나의 url에 대해 http 메소드(get, post, put, patch, delete 등)를 다르게 사용해 여러 개의 뷰를 처리하도록 지원한다

그리고 ViewSet은 여기서 더 나아가 여러 개의 url 패턴에 대해 여러 개의 http 메소드를 사용해 여러 개의 뷰를 처리한다
ViewSet 은 말그대로 View들의 Set 즉, 요청과 응답에 사용되는 여러 View들이 모여있다는 의미이다

가장 기본적으로 사용하는 ViewSet이 바로 ModelViewSet이다

ModelViewSet은 기본적으로 Retrieve, List, Create, Destroy, Update의 뷰를 제공한다

이번 미션은 우선 이번 미션은 CBV로 view를 구현한 뒤, viewset으로 리팩토링하는 것이었다

# views.py

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

queryset은 해당 모델의 객체들을 모두 가져오고, serialzer_class는 해당 모델의 Serializer를 가져온다

ViewSet에서 제공하는 각각의 view들은 다음과 같이 url들과 매핑되도록 되어있다

  • Retrieve, Update, Destroy 뷰: model_name/<int:pk>/와 같은 형태의 url을 사용해 해당 pk에 해당하는 모델 인스턴스를 보여주거나, 업데이트 하거나, 제거해주는 역할
  • List, Create 뷰: model_name/와 같은 형태의 url을 사용해 해당 모델의 여러 인스턴스를 보여주거나, 새로운 인스턴스를 만들어주는 역할

참으로 신기하다...!


이렇게 자동으로 url이 매핑되려면 urls.py에 router를 등록해주어야한다

from rest_framework import routers
from .views import ModelNameViewSet

router = routers.DefaultRouter()
router.register(r'model_name', ModelNameViewSet)

urlpatterns = router.urls

urlpatterns = [
    path('', include(urlpatterns))
]

4. Filtering

filtering은, 어떤 queryset에 대하여 원하는 옵션대로 필터를 걸어, 해당 조건을 만족하는 특정 queryset을 만드는 작업이다

예를 들어, items/라는 url 패턴을 갖는 list view가 있다고 하고, 클라이언트가 items/?name=ceos라는url에 접근한다고 하면, ceos라는 name 속성을 갖는 item들의 쿼리셋이 담긴 response를 클라이언트에게 돌려주게 된다

DRF는 이런 필터링 기능을 filterset으로 지원해준다

# views.py

class BoardFilter(FilterSet):
    school = filters.NumberFilter(field_name='school_id')

    class Meta:
        model = Board
        fields = ['school']


class BoardViewSet(viewsets.ModelViewSet):
    queryset = Board.objects.all()
    serializer_class = BoardListSerializer

    filter_backends = [DjangoFilterBackend]
    filterset_class = BoardFilter

단순한 형태로 기존 모델의 field 값과의 일치여부만 파악하는 filter라면 filterset_fields라는 속성을 사용할 수도 있고,
FilterSet을 구현해 사용하는 경우, method를 이용해 다른 로직을 구현할 수도 있다



5. 회고

먼저 api 명세서를 만들고, CBV 방식으로 신나게 코드를 짰다(이때까지는 즐겁기만 했음)
하지만 ViewSet으로 리팩토링 하는 것에서 고난과 역경을 겪었다...
기껏 짜놓은 api들이 망가져서 마음이 아팠다
Django는 편리한 기능이 많은 것은 사실이지만,
이것을 잘 사용하려면 기본 기능을 잘 알아야될 것 같다
그래야지 커스텀해서 코드 재사용성을 높일 수 있으니껭!

깃헙 리드미에 리팩토링 힘들었다고 했더니 커스텀이 필요한 부분은 CBV를 사용하고, 그 외는 ViewSet을 사용해서 간단하게 구현하면 된다는 답변을 받았당!
그럼 끝~~



profile
핸수

0개의 댓글