Django, DRF Serializers - serializer 파헤치기, 왜 serializer? response가 만들어지기 까지

정현우·2023년 2월 16일
8

Django Basic to Advanced

목록 보기
20/37

[ 글의 목적: drf에서 DTO 그 이상의 역할을 하는 serializer 의 기본적인 사용법과 그 이상의 활용법을 이해하기 위해 ]

DRF Serializers

아주 간단한 단일 모델 CRUD 시리얼라이저 활용법은 이전 글에 있다. 하지만 serializer를 보다 본격적으로 사용하는 방법에 대해 고민 해 보자. 해당 글은 이미 django, drf를 한 번은 사용한 사람들을 위한 글이다.

1. Serializers 를 왜써야 할까

  • serializer가 뭔지 다시 생각해보자. (1) 존재 이유는 무엇인가? (2) 혹시 drf를 사용하는 여러분은 django가 가진 model form을 사용해 본적이 있는가? (3) json type의 restful 한 API를 만들때 꼭 drf가 필요한가?

  • 아쉽게 대부분의 경우에서 위에서 언급한 부분을 놓치고 바로 도입하는 경우가 많다. django는 태생이 full-stack framework이며 애초에 CMS컨셉을 가져갔다. FE를 rendering해주기 위해 template, template-engine이 있고 우리는 그 MVT pattern을 따라서 full-stack django app을 만들었어야 했다.

  • template-engine 은 java에서 jsp, js(node runtime) express에서 pug또는 ejs 등과 같은 여타 template-engine 과 크게 다르지 않다. raw html에 {% if article_list %} 와 같은 template 문법으로 SSR(Server Side Rendering) 해서 response로 html file을 주는 것이다.

  • 그 과정에서 중복되는 header, footer 분리, 그리고 기본적인 보안을 위한 csrf token, 등이 있고 또 특정 모델(object)을 리스트 형태로 또는 단일 모델을 보여줘야 하는 경우가 당연히 필요했다. /post/1 로 요청을 주면 pk가 1인 post를 하나 리턴하거나 /posts/ 로 요청을 주면 pagination이 된 상태로 post들을 array형태로 리턴하는 경우가 web-application이니까 필연적으로 필요하다.

  • Django의 강력한 ORM 기능을 바탕으로 queryset을 구성하고 적절한 CRUD 기능이 필요했다. 그래서 View 라는 개념의 컨셉으로 http protocol request를 method (get, post, patch, put, delete ...) 에 맞게 대응해 주는 것이 있다. 대표적으로 아래와 같은 BaseListView 가 존재한다.

class BaseListView(MultipleObjectMixin, View):
    """A base view for displaying a list of objects."""

    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()

        if not allow_empty:
            # When pagination is enabled and object_list is a queryset,
            # it's better to do a cheap query than to load the unpaginated
            # queryset in memory.
            if self.get_paginate_by(self.object_list) is not None and hasattr(
                self.object_list, "exists"
            ):
                is_empty = not self.object_list.exists()
            else:
                is_empty = not self.object_list
            if is_empty:
                raise Http404(
                    _("Empty list and “%(class_name)s.allow_empty” is False.")
                    % {
                        "class_name": self.__class__.__name__,
                    }
                )
        context = self.get_context_data()
        return self.render_to_response(context)
  • 위와 같은 View 를 상속받아서 아래와 같이 우리가 원하는 값(attribute)만 재정의 해주면 아주 쉽게 사용할 수 있다.
class ArticleListView(generic.ListView):
    model = Article
    context_object_name: str = "article_list"
    template_name: str = "articleapp/list.html"
    paginate_by: int = 5
  • 근데 문제가 하나 생긴다. 제일 처음 마주하는 문제는 create action (post request) 을 위해 FE(html)에서 form tag로 만들었는데 "특정 값만 받으면 되는 경우" 이다. 로그인 이후 게시글 하나 쓸 때 "생성자(user)"나 "조회수"나 이런 값들은 BE 단에서 control하면 되는데 FE에서 던져주는 것도 이상하다. 그리고 web은 전쟁터다. form값에 대한 1차(FE) 2차(BE) vaildation은 너무나도 필수다.

  • 이 것을 위해 django에서는 Model Form 이 있다. BaseModelForm 을 꼭 한번 살펴보길 추천한다. 위에서 언급한 과정을 django core에서 만들어 놓은 좋은 BaseModelForm - ModelForm class를 상속 받아서 아래와 같이 사용한다.

class ArticleCreationForm(ModelForm):
    class Meta:
        model = Article
        fields = ["title", "image", "content"]
  • 굉장히 장고 철학스러운 방법이다. 장고는 많은 부분이 "템플릿 메소드 패턴" 디자인 패턴으로 상위 class를 만들어 두었다. 하지만 이렇게 full-stack으로 제공하기 위한 것들이 django를 rest-full 한 API를 만들게 하기 위함으로써 걸림돌이 되었다.

  • 그럼에도 왜? 왜 django을 선택할까? 생각해보면 아무래도 에자일한 개발이 필요한 상황에 model에 대한 admin까지 간편하게 바로 만들어주고 (게다가 action은 강력한 admin 도구로 사용할 수 있다) rest-full 한 API 까지 만들 수 있는 선택의 최적은 django 이었을 것이다. 필자도 그렇게 생각해서 django를 많이 선택했다.

  • 하지만 django ORM이랑 결합해서 response는 json이라는 data type이 필요하게 되었고 기존에 model form이 가지는 concept 또한 필요하게 되었다. 그렇게 drf과 같은 형태의 라이브러리가 필요하게 된다. 우선 drf없이 django로 rest-full한 api를 만들려면 어떻게 해야할까?

  • django에는 (django > http) JsonResponse 가 이미 존재한다. 하지만 json request는? 어떻게 django ORM으로 casting해서 ORM을 잘 활용할까? 아래 3개의 글을 한 번 체크해 보자. 마지막 글은 drf에 대한 내용도 있다.

  1. https://dev.to/alexmercedcoder/creating-a-restful-api-with-django-without-djangorestframework-17n7

  2. https://medium.com/geekculture/building-django-api-views-without-django-rest-framework-4fa9883de0a8

  3. https://www.quora.com/Is-it-smart-to-build-a-REST-API-without-the-Django-REST-framework

  • drf 는 django base로 하는 또 다른 web-framework이다. rest-full concept에 맞지 않는 django를 그나마 rest api concept에 맞춰서 사용할 수 있는 것들을 많이 제공해 준다. 그래서 우리는 마치 model-form을 사용하는 것 처럼 drf를 사용할 수 있다. full-stack으로 개발하던 django의 관성을 해치지않고 동시에 rest-full 하게 api 도 만들 수 있으니 최적의 선택이 되는 것이다.

  • 하지만 아쉽게, 이를 통해 놓치게 되는 기본 개념도 많다. 그래서 많이들 web 공부는 jvm 언어를 선택해서 시작하는 것이 그나마 좋다고 추천하는 것 같다.

1) 단일 Serializers

(1) Serializer

  • 가장 흔하게 보게되고 기본적인 serializers.Serializer 부터 살펴보자. model 예시는 아래 CheckedCrn 이라는 model을 가지고 하겠다.
class CheckedCrn(models.Model):
    registration_number = models.CharField(
        max_length=20, unique=True, blank=False, null=False, verbose_name="사업자등록번호"
    )
    is_closed = models.BooleanField(blank=False, null=False, verbose_name="사업자 폐업여부")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
  • 이런 field를 가지는 model을 get all 하는 api를 만들기 위해 serializer를 만들어 보자. (왜 시리얼라이저를 만들어야 하는가? 에 물음은 이미 위에서 언급되었다.)
class CheckedCrnSerializer(serializers.Serializer):
    registration_number = serializers.CharField(max_length=20)
    is_closed = serializers.BooleanField()
    created_at = serializers.DateTimeField()
    updated_at = serializers.DateTimeField()
  • 아주 심플하게 이렇게 구성할 수 있다. 이것을 dataclass, DTO 등과 같이 활용할 수 있다. registration_number 를 그냥 다른 변수 명으로 변경해도 괜찮고 (model과 형태만 같다면), 아니면 또 다른 나만의 field를 추가할 수 있다. fixed_name = serializers.CharField(default="FIX") 를 추가해 보자. 그러면 response 도 해당 field가 추가되어서 준다. 이렇게 data model 과 같이 자유롭게 활용할 수 있다. pydantic 이 떠오르는 사람은 이 글을 한 번 읽는 것을 추천한다.

  • 그리고 Django ORM Model <-> Serializer 가 자유롭다. ORM이 아니더라도 아래를 보면 dict 등을 통해 validation을 해볼 수 있다.

# 모델과 시리얼라이즈
>>> from apis.test.models import CheckedCrn
>>> target = CheckedCrn.objects.get(id=199)
>>> CheckedCrnSerializer(target)
CheckedCrnSerializer(<CheckedCrn: 3124354: closed >):
    registration_number = CharField(max_length=20)
    is_closed = BooleanField()
    created_at = DateTimeField()
>>> target_se = CheckedCrnSerializer(target)
>>> target_se.data
{'registration_number': '3124354', 'is_closed': True, 'created_at': '2023-01-15T03:55:49.974890'}


>>> test_request = {
...     "registration_number": "123",
...     "is_closed": False
... }
>>> t= CheckedCrnSerializer(test_request)
>>> t.data
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/rest_framework/fields.py", line 457, in get_attribute
    return get_attribute(instance, self.source_attrs)
  File "/usr/local/lib/python3.8/site-packages/rest_framework/fields.py", line 95, in get_attribute
    instance = instance[attr]
KeyError: 'created_at'
... (생략)
  • 그럼 이렇게 만든 serializer로 어떻게 django view에서 활용할 수 있을까? django의 view를 통해서 request, response를 json -> model -> ... -> josn 시리얼라이징하면 된다. 사실 이렇게 직접 할꺼면 drf, Serializer를 활용하는 의미가 없다. 그래서 drf에서는 GenericAPIView를 제공하는 것이다. 그 중 generics.ListAPIView 를 활용해 아래와 같이 view를 구성하면 rest-api 구성은 끝이난다.
class CheckedCrnListAPIView(generics.ListAPIView):
    """
    - CheckedCrn 모델 GET ALL API
    """
    queryset = CheckedCrn.objects.all().order_by("-id")
    serializer_class = CheckedCrnSerializer
  • 말이 안되게 간단하다. django를 사용하는 관성을 헤치지 않고 마치, 우리가 django를 가지고 full-stack 개발을 하는 것 처럼, model form을 활용하는 것 처럼, 개발을 할 수 있는 것이다. (Serializer의 Response는 django의 SimpleTemplateResponse 를 활용한다.)

(2) ModelSerializer

  • 허나 일반적인 Serializer를 쓰다보니까 번거로움이 생긴다. "아니 model에 이미 적혀져 있고, 선언되어 있는 ORM field가 있는데 그걸 또 똑같이 다시 써야해??" 사실 위 시리얼라이저는 ORM을 위한 것이 아니다. django model을 위한 Serializer는 바로 ModelSerializer 이다. 위 CheckedCrnSerializer 를 아래와 같이 아주 간단하게 바꿀 수 있다.
class CheckedCrnSerializer(serializers.ModelSerializer):
    class Meta:
        model = CheckedCrn
        fields = "__all__"
  • 아주 간단해졌다. 이게 끝이다! 만약 모든 fields를 response로 주고 싶지 않다면 fields = "__all__" 부분을 exclude = ("id",) 로 바꿔보자! 이렇게 field control이 간단하다.

  • 근데 갑자기 이런 기능이 고려된다. "아 비즈니스로직이랑 상관없이 response 줄 때 그냥 특정 값에 따라 dynamic하게 모두 바꿀 순 없나?" SerializerMethodField 를 사용할 때 다.

(3) SerializerMethodField

  • CheckedCrnSerializer 를 그대로 가져와 registration_number 의 길이가 4 보다 크면 is_long 이라는 field값을 추가해서 "long" 으로 주고 싶다. 그렇지 않으면 "short" 으로 주자!
class CheckedCrnSerializer(serializers.ModelSerializer):
    is_long = serializers.SerializerMethodField()

    class Meta:
        model = CheckedCrn
        exclude = ("id",)

    def get_is_long(self, instance: CheckedCrn):
        if len(instance.registration_number) > 4:
            return "long"
        else:
            return "short"
  • 간단하다. 기본 Serializer classserializers.SerializerMethodField() 를 추가하고 이름값 답게 get_[field_name...] 으로 method를 만들어서 정의 해 주자. 얘는 고정 파라미터값으로 2번째 instance를 받는데 Serializer에 사용되는 model을 의미한다. 솔직히 영리하게 잘 만들었다고 생각이 들었다.

2) 다중 Serializers, nested Serializers

(1) 1:N 일 때 1의 입장에서

class Product(models.Model):
    name = models.CharField(
        max_length=20, blank=False, null=False, verbose_name="제품 이름"
    )
    price = models.PositiveIntegerField(blank=False, null=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
class Cart(models.Model):
    uesr = models.ForeignKey(User, on_delete=models.CASCADE, blank=False, null=False)
    product = models.ForeignKey(
        Product, on_delete=models.CASCADE, blank=False, null=False
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
  • 위와 같은 모델이 두 개 있다고 치자. Product 1 : N Cart 형태의 관계의 model이다. (참고로 일단 cart에서 FK인 Product에는 related_name 를 주지 않았다.)

  • 그리고 serializers 를 다음과 같이 구성하고 ListAPIView를 만들어 보자.

class CartSerializer(serializers.ModelSerializer):
    class Meta:
        model = Cart
        fields = "__all__"


class ProductSerializer(serializers.ModelSerializer):
    cart_set = CartSerializer(many=True, read_only=True)

    class Meta:
        model = Product
        exclude = ("id",)
        
# view
class ProductListAPIView(generics.ListAPIView):
    """
    - Product n Card GET ALL API
    """
    queryset = Product.objects.all().order_by("-id")
    serializer_class = ProductSerializer

  • product 입장에서 자신을 참조하고 있는 cart를 다 가져와서 보여주는 형태가 된다. 참고로 앞서 언급한 model정의에 FK 줄 때 related_name 를 주지 않는다면 역참조시에 해당 model이름_set 가 default로 세팅이 된다. 고로 웬만하면 related_name 을 주는 것이 협업시 혼선을 줄일 수 있다. 또한 이 역시 prefetch_related / select_related 에 선택되는 field 이름에 영향을 준다. (역, 정참조에 대한 글은 해당 시리즈에 있다.)

  • 이렇게 N 입장인 cart의 serializer를 재사용, 활용하여 product에게 cart_set라는 field로 넘겨주면 아무런 추가 작업 없이도 nested 구성이 가능하다. 그러면
    "Create" 일 땐 어떨까?

class ProductSerializer(serializers.ModelSerializer):
    cart_set = CartSerializer(many=True)

    class Meta:
        model = Product
        exclude = ("id",)
        
# view
class ProductListAPIView(generics.ListCreateAPIView):
    """
    - Product n Card GET ALL API & Create
    """

    queryset = Product.objects.all().order_by("-id")
    serializer_class = ProductSerializer
  • 그냥 simple하게 생각해 보면 위에서 사용한 시리얼라이저 그-대로 사용하되 APIView에 ListCreateAPIView 를 사용하면 되지 않을까 생각이 든다. 응 안됀다.

  • 기본적으로 nested serializer는 읽기 전용이다. 그리고 읽기 전용이면서 not - nested create로 product serializer를 재활용 하고 싶으면 cart_setread_only=True 값을 살려두고 사용하면 product 단일 create로 사용이 가능하다.

  • create 할 땐 serializer에서 create를 override 해서 정의를 해 줘야한다. 그리고 사실 create에 비즈니스 로직이 추가가 된다. 솔직히 create 할 때 시리얼라이저에서 역 또는 정참조의 model까지 묶어서 create를 하는 행위는 그렇게 동의하지는 않는다. 비즈니스 로직이 있어야 할 view 에서 또는 mixin or manager 로 등 과 같이 해당 case에 대응해서 개발할 수 있는 좋은 방법과 수단이 많다.

  • 그래도 아래 글 을 꼭 한 번 읽어보자

  1. https://django.cowhite.com/blog/create-and-update-django-rest-framework-nested-serializers/

  2. https://hyeo-noo.tistory.com/300

(2) 1:N 일 때 N의 입장에서

  • 이 경우는 너무 심플하다. 특별한 추가 작업 없이 사용을 원한다면 아래와 같이 구성하면 끝이다.
class ProductOnlySerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = "__all__"


class CartWithProductSerializer(serializers.ModelSerializer):
    product = ProductOnlySerializer(read_only=True)

    class Meta:
        model = Cart
        fields = "__all__"

# view
class CartListAPIView(generics.ListAPIView):
    """
    - Cart GET ALL API
    """
    queryset = Cart.objects.all().order_by("-id")
    serializer_class = CartWithProductSerializer

  • 어짜피 cartproduct 가 존재한다. 왼쪽이 nested serializer 를 세팅안한 결과값이고 오른쪽이 nested serializer 결과값이다. 이미 존재하는 productProductOnlySerializer 로 이어주면 깔끔하게 끝난다.

  • 그리고 serializer는 create, update 등의 method를 override 해서 다양한 활용을 할 수 있다. 대표적으로 model field의 modify 날짜가 아닌, 특정 값이 update되었을 때 serializer에서 update 를 활용해 last update date 를 now로 수정할 수 있다.

  • 하지만 필자는 serializer는 그 본연, 자체일때 더 의미가 있다고 생각한다. django가 아주 다양한 활용법이 존재하고 python 특성상 자유성을 추구한다고 하지만 규모있는 작업을 위해 협업을 하기위해 사용한다면 serializer에 복잡한 logic을 추가하는 것은 고민을 할 필요가 있어 보인다. 특히 django admin과 rest api를 위해 drf를 사용하는 경우는 더욱 더 말이다.

3) List Serializers

  • drf에서는 create는 기본적으로 단일 모델 대상 생성만 제공한다. bulk create등과 같은 logic을 활용하고 싶으면 list serializer를 직접 만들어야 한다. 근데 사실,, bulk create는 serializer level에서 handling하는 것이 아니라 view level에서 create many field를 활용하는 것이 낫다.
class CartCreateAPIView(generics.CreateAPIView):
    """
    - Cart Bulk Create API
    """

    queryset = Cart.objects.all().order_by("-id")
    serializer_class = CartSerializer

    def create(self, request: Request, *args, **kwargs) -> Response:
        kwargs["many"] = isinstance(request.data, list)
        serializer = self.get_serializer(data=request.data, **kwargs)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)
  • kwargs["many"] = isinstance(request.data, list) 를 통해 list 일때 아닐때 구분해 bulk create or create one 으로 분기할 수 있다. 오히려 serializer가 필요할 땐 bulk update 이다.

  • 하지만 이 bulk update도 serializer는 굉장히 난해하다. 왜 bulk update를 해야하는가? 데이터 정합성에 대한 고려도 필요하지 않는가? 사실 중요한 data는 bulk update를 단순화 또는 단일화해서 돌리지 않을 것 이다. dump로 update가 필요한 순간은 온다.

    • 예로 queryset으로 묶어서 100개씩 들고와서 "처리 상태를 bulk-update" 로 해버리는 로직을 생각해보자. for in 으로 처리할 수 있지만 clean하게 로직을 짜는게 쉽지는 않다.
    • 물론 serializer를 활용해도 사실 내부로직은 for loop를 도는 것과 동일하다.
  • List data type의 serializer들은 직접 만들어서 사용하는 것이 기본적이다. 아래 github repo와 참고 링크를 꼭 같이 살펴보는 것을 추천한다.

  • https://github.com/cdknorow/django_bulk_tutorial/tree/master/datamanager

  • https://medium.com/swlh/efficient-bulk-create-with-django-rest-framework-f73da6af7ddc


2. Serializers core

1) django request flow

  • 시리얼라이저의 코어에 한 칸 더 다가가 보자. 가장 먼저 마주하게 되는 부분은 아마 비즈니스로직 부분도 아닌 단순 데이터 시리얼라이징, serializers.Serializer 일 것이다. (가상환경)/site-packages/rest_framework/serializers.py 경로에 위치하며 가장 핵심은 BaseSerializer 인 것을 볼 수 있다.

  • django request flow는 기본적으로 http request -> ...서버 및 네트워크 기기 생략... -> wsgi (or 구현체에 따라 asgi) -> HttpRequest -> django middle-ware ... -> [ 비즈니스로직 ] -> HttpResponse -> django middle-ware ... -> wsgi handler -> http response 이다.

  • 여기서 집중해서 볼 부분은 [ 비즈니스로직 ] 처리 부분이다. drf와 함께, 시리얼라이저와 from rest_framework import generics 에서 view를 가져와서 사용할 것 이다. 그리고 그 genericsclass GenericAPIView(views.APIView) 를 말하며 이 또한 class APIView(View) 를 상속 받고 있다. (가상환경)/site-packages/rest_framework/views.py 에 위치하는 이 친구가 가장 시발점이되는 부모 class 이다. 여기서 상속받아서 활용하는 class는 django core의 View class 이다.

  • 기본 장고의 View class를 inheritance하고 override한다.

  • 사진과 같으며 아마 직접보면 알 수 있듯 거의 Interface역할을 하고 있다. 이 APIView 를 기반으로 GenericAPIView 를 만들고, CRUD 역할과 실제 create, update, partial_update, list, retrieve, destroy 행위를 하는 함수는 django에서 흔히볼 수 있는 Mixin을 활용한 패턴으로 되어 있다. (어떤 mixin 들이 있는지 해당 시리즈에서 crud에 대한 실습 글 을 보면 빠르게 확인가능하다.)

2) drf request flow

  • 하나의 예시를 들어보자, 위에서 든 예시로 generics.ListAPIView 를 상속받아서 CartListAPIView 를 만들었다. url(end-point)을 정의하는 부분에서 path("carts/", CartListAPIView.as_view(), name="cart-list-apiview"), 와 같이 정의했다. 이 as_view 는 위 사진에서 볼 수 있는 @classmethod - as_view 를 호출한다. 그리고 잘 보면 부모의 as_view 를 호출하는 것을 볼 수 있다.

  • 여긴 django core에서 view를 return하는 factory method pattern 인 것을 알 수 있다. 이 view에 의해서 결국 drf > generics > ListAPIView class의 def get > def list 를 return하게 되는 것을 알 수 있다.

  • http_method_names 에는 http_method_names = ["get", "post", "put", "patch", "delete", "head", "options", "trace",] 이 정의되어 있으며 이는 역시 ListAPIView 에서 정의되어 (get으로) 있다.

class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    """
    Concrete view for listing a queryset.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

...

### list 정의 부분
class ListModelMixin:
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)
  • 멀리 돌아왔지만 최종적으로 Response object가 만들어지는 부분을 드디어 catch 했다. (가상환경)/site-packages/rest_framework/response.pyclass Response(SimpleTemplateResponse) 임을 알 수 있다. SimpleTemplateResponse 역시 django core의 HttpResponse을 상속받고, 다시 HttpResponseBase를 상속받는 것을 알 수 있다. 결론적으로 drf는 django 기본 request - response flow 사이에 껴 들어서 SimpleTemplateResponse 를 활용해 시리얼라이징 되는 response를 만드는 것을 알 수 있다.

3) drf serializer 가 response 만들기 까지

  • 위 (2)에서 Response(serializer.data) 로 return 하는 것 까지 확인했다. 이제 serializer 가 어떻게 만들어지는지 다시 처음부터 체크해 보자. serializer는 GenericAPIView 에서 get_serializer, get_serializer_class function에 의해 만들어 진다.

  • ListModelMixin 에서 serializer = self.get_serializer(queryset...) 부분은 결국 serializer class의 생성자를 호출하며 그 instance를 바로 return하는 것을 알 수 있다. 이 코드에서 serializer는 결국 CartListAPIView 를 예시로 들었던 것에서 알 수 있듯 class CartSerializer(serializers...) 의 instance임을 알 수 있다.

  • serializers.SerializerBaseSerializer 를 상속받고 있는 것을 확인했다. 그리고 (아래 1번사진) 생성자(\__init\__)를 타고 > data > get_initial function까지 follow 하면, (아래 2번사진) class Serializer(BaseSerializer, metaclass=SerializerMetaclass) 의 진짜 serializer instance를 만들고 Response(serializer.data) 하는 부분의 data를 만들어 주는 찐본체에 접근할 수 있다.

  • 그리고 serializers data는 vaildation을 포함한 많은 데이터 전처리를 거친 response json에 가까운 dict형태임을 알 수 있다. 이놈의 OrderedDict는 자료 구조얘기도 나올 것 같아서 다루지 않겠다. 우리가 단순하게 view & serializer 에만 집중해서 rest-ful한 json response를 만들 수 있는 back-ground에는 이렇게 많이 추상화된 부분이 있는 것을 알 수 있다.

  • 굉장히 상호보완적으로, 객체지향 프로그래밍의 관심사대로 잘 구조화되어 있는 것을 알 수 있다. (개인적으로 type hint들이 조금 더 많이 추가되면 매우 좋을 것 같다ㅎ..)


3. 마무리

  • drf view 계층은 크게 위와 같은 구조로 되어 있다. CBV 위주로만 살펴보았지만, 기본적으로 FBV 또한 method 형태일 뿐 거의 동일하다. (FBV도 데코레이터를 활용하기 때문이다)

  • 이 글을 다 봤으면, drf 에서 제공하는 것이 아닌, 자체 serializer를 만들어 활용할 수 도 있는 것을 알았을 것이다. model serializer를 상속받아서 본인만의 base-serializer를 만들어 사용하는 것도 좋은 접근법이라고 생각이 된다.

  • 생각보다 추상화되어서 분리되어 있는 것들이 많아서 back tracing하기 힘들었다. 하지만 역시, 그래도 잘만들어진 framework가 아닌가 생각이 든다. 협업을 위해서 관심사를 철저하게 framework를 "활용"하고 그 규칙에 따라서 "비즈니스 로직에만 집중" 할 수 있게 해서 일관성 있고, 협업가능한 구조를 가져갈 수 있게 해준다.

  • 해당 글이 완벽할 수 없기에 잘못 설명하고 있는 부분이나, 잘못 tracing 하는 부분이 있으면 꼭 알려주세요!!


출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

0개의 댓글