장고 시작하기 26. DRF – 계층 구조 JSON 응답하기

장고 serializer의 강력한 기능 중 하나는 Nested 관계를 처리하는 기능으로, 관련 개체가 있는 데이터 구조를 직렬화 및 역직렬화할 수 있습니다. 이 기능은 Nested 관계의 데이터를 손쉽게 “계층 구조 JSON” 으로 응답할 수 있게 됩니다.

이번 포스트에서는 DRF에서 Nested Relationship은 무엇이며 어떻게 다루고 어떻게 “계층 구조 JSON”으로 응답할 수 있는지 공부한 내용을 정리해보았습니다.

Nested Relationship이란?

Nested Relationship는 Django 모델의 개체가 다른 개체와 연관 관계를 갖는 시나리오를 나타내며 이러한 관계는 계층 구조로 표현될 수 있습니다.

예를 들어, 블로그 게시물을 나타내는 모델이 있고 각 게시물에 연결된 댓글이 있는 구조를 가정해 봅니다.

ERD는 다음과 같이 표현 될 수 있습니다.

이에 해당하는 장고 모델을 다음과 같이 만들 수 있고요.

from django.db import models


class Articles(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)


class Comments(models.Model):
    article = models.ForeignKey(
        Articles, on_delete=models.CASCADE, related_name="comments")
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

1:N관계를 생성하기 위해 N에 해당하는 모델에 models.ForeignKey로 Articles 모델을 참조하는 것에 주목합니다.

여기서 Nested relationship을 만든다는 것은 응답하는 JSON의 구조를 참조한 테이블과 참조된 테이블이 모두 나올 수 있도록 “계층 구조 JSON”으로 변경하는 것 입니다.

그러면 게시물과 댓글 사이에 상위-하위 관계가 생성되어 Nested 구조가 형성되게 되어 상위 테이블만 조회해도 하위 테이블 내용도 같이 조회 할 수 있게됩니다.

결과적으로 아래와 같이 Articles 모델만 조회해도 Comment 모델의 내용도 같이 API로 뿌려줄 수 있게 되는거죠

serializer에서 nested 관계를 처리하는 방법

Django serializer에서 이러한 nested 관계를 처리하려면 각 관련 모델에 대한 serializer를 정의한 다음 상위 serializer 내에서 이러한 serializer를 선언해야 합니다.

게시물 예제를 계속 진행해 보겠습니다.

상위 serializer(예: ArticleSerializer) 내에서 관련 모델에 대한 필드를 포함하는 해당 nested serializer를 선언합니다.

댓글의 경우 ArticleSerializer 내에서 CommentSerializer를 선언합니다.

Nested 구조를 만들지 않은 기존의 코드

from rest_framework import serializers
from .models import Articles, Comments


class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Articles
        fields = "__all__"


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comments
        fields = "__all__"
        read_only_fields = ("article",)

게시글 조회 응답 JSON

{
    "id": 3,
    "title": "Choice standard forward.",
    "content": "Action central similar boy certainly. Certainly cost health scene exactly order. Prepare especially face specific than hundred top.",
    "created_at": "1972-05-01T00:40:37.736189+09:00",
    "updated_at": "1991-03-22T23:24:19.690504+09:00"
}

serializer의 nested 관계 변경

상위 serializer에 nested serializer을 넣기 위해 serializer class 선언의 위치를 변경한 것에 유의해주세요.

모델에서 related_name을 입력한 것 처럼 하위 serializer를 변수로 넣어줍니다.

Article serialzer로 POST 시에도 nested 관계인 Commentserilizer 를 모두 건들 수 있기 때문에 read_only=True 인자를 전달해 줌에 유의해야 합니다.

from rest_framework import serializers
from .models import Articles, Comments


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comments
        fields = "__all__"
        read_only_fields = ("article",)

class ArticleSerializer(serializers.ModelSerializer):
    comments = CommentSerializer(many=True, read_only=True)
    class Meta:
        model = Articles
        fields = "__all__"

게시글 조회 응답 JSON

위와 같이 시리얼라이져만 변경해 주었는데 데이터가 자동으로 계층화 되어서 표현되는 것을 확인 할 수 있습니다.

{
    "id": 3,
    "comments": [
        {
            "id": 21,
            "content": "새로운 댓글을 추가합니다.",
            "created_at": "2024-04-24T14:37:00.958958+09:00",
            "updated_at": "2024-04-24T14:37:00.958984+09:00",
            "article": 3
        }
    ],
    "title": "Choice standard forward.",
    "content": "Action central similar boy certainly. Certainly cost health scene exactly order. Prepare especially face specific than hundred top.",
    "created_at": "1972-05-01T00:40:37.736189+09:00",
    "updated_at": "1991-03-22T23:24:19.690504+09:00"
}

Nested Serializer의 쿼리 결과 개수도 전달하기

게시글 상세 조회만 하더라도 해당하는 댓글들과 그 수도 응답할 수 있도록 변경해야 한다고 가정해봅니다.

이는 아래 코드를 사용해서 구현 할 수 있습니다.

comments_count = serializers.IntegerField(source="comments.count", read_only=True)

  • 다양한 방법으로 nested data의 개수를 구할 수 있지만 IntegerField를 추가 해줌으로써 구현할 수 있습니다.
  • source 인자로 개수를 세는 ORM인 .count을 전달해서 nested 데이터의 개수를 반환해줄 수 있습니다.
from rest_framework import serializers
from .models import Articles, Comments


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comments
        fields = "__all__"
        read_only_fields = ("article",)


class ArticleSerializer(serializers.ModelSerializer):
    comments = CommentSerializer(many=True, read_only=True)
    comments_count = serializers.IntegerField(source="comments.count", read_only=True)
    class Meta:
        model = Articles
        fields = "__all__"

응답 JSON

{
    "id": 3,
    "comments": [
        {
            "id": 21,
            "content": "새로운 댓글을 추가합니다.",
            "created_at": "2024-04-24T14:37:00.958958+09:00",
            "updated_at": "2024-04-24T14:37:00.958984+09:00",
            "article": 3
        }
    ],
    "comments_count": 1,
    "title": "Choice standard forward.",
    "content": "Action central similar boy certainly. Certainly cost health scene exactly order. Prepare especially face specific than hundred top.",
    "created_at": "1972-05-01T00:40:37.736189+09:00",
    "updated_at": "1991-03-22T23:24:19.690504+09:00"
}

계층 구조 JSON 만들 시 주의사항

Nested serializers 1:N 관계의 모델을 계층적으로 만들어서 응답해주기 때문에 한 번에 연관된 데이터를 쉽게 얻을 수 있게 되는 것 같습니다.

하지만 Nested serializers를 만들 때 주의해야 하는 점이 있는데요.

아래의 코드를 보면서 소름 돋는 점이 있지 않을까요…?

class ArticleListAPIView(APIView):
    def get(self, request):
        articles = Articles.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)
...


class ArticleDetailAPIView(APIView):
    def get_object(self, pk):
        return get_object_or_404(Articles, pk=pk)

    def get(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article)
        return Response(serializer.data)
...

위의 view 코드는 글 목록과 글 상세 API에서 모두 글과 관련된 댓글들을 함께 응답하도록 설계 되어 있습니다.

글 목록에서는 1:N 관계의 댓글을 굳이 다 표현할 필요가 없는데 말이죠….

이는 간단한 요청에도 nested 관계의 모든 데이터까지 가져오게 되는 불상사를 발생 시킵니다.

Serializer 상속

이런 경우 글 목록과 글 상세에서 모두 동일한 serializer를 사용하기 때문에 to_representation() 와 같은 함수로 댓글에 대한 응답을 뺄 수도 없습니다.

대신 이런 경우에는 2개의 시리얼라이져를 구분해서 만들면 되는데요.

<기존>

from rest_framework import serializers
from .models import Articles, Comments


class CommentSerializer(serializers.ModelSerializer):
    ...


class ArticleSerializer(serializers.ModelSerializer):
    comments_count = serializers.IntegerField(
        source="comments.count", read_only=True)

    class Meta:
        model = Articles
        fields = "__all__"


class ArticleDetailSerializer(ArticleSerializer):
    comments = CommentSerializer(many=True, read_only=True)

<nested 관계 시리얼라이져는 상속으로 분리하도록 변경>

2개 모두 동일하게 글과 관련된 시리얼라이져인데 comment 부분의 Nested 관계만 다르기 때문에 상속을 이용해서 이를 분리하여 구현할 수 있습니다.

이제 view에서 nested 관계 내용은 DetailSerializer로 처리하도록 변경해주면 됩니다.

class ArticleListAPIView(APIView):
    def get(self, request):
        articles = Articles.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)
...


class ArticleDetailAPIView(APIView):
    def get_object(self, pk):
        return get_object_or_404(Articles, pk=pk)

    def get(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleDetailSerializer(article)
        return Response(serializer.data)
...

이렇게 되면 문제 없이 요청에 따라 Nested 관계 모델을 계층형 데이터로 응답할 수 있게 됩니다.

계층 구조 JSON 장점

위에서도 언급했지만 Nested 관계를 만들어 계층 구조로 응답하면 아래와 같은 장점을 가져갈 수 있습니다.

향상된 데이터 표현

  • Nested 관계를 통해 계층적 데이터 구조를 보다 자연스럽게 표현할 수 있습니다.

복잡성 감소

  • 상위 개체 내에 관련 개체를 캡슐화함으로써 중첩 관계는 데이터 조작을 단순화하고 중복성을 줄이는 데 도움이 됩니다.

마치며

저는 업무상 서버에서 “DB 데이터 -> 직렬화 -> 응답” 하는 과정보다는 클라이언트 측에서 “JSON -> 데이터 모델링 -> DB에 load” 하는 과정에 더 익숙했습니다.

API 제공 측에서 DB 모델을 공개하지 않는 이상 복잡한 계층의 JSON 데이터를 모델링하는 일은 쉽지 않았는데요…

백엔드에서 계층 구조로 직렬화하여 응답해주는 과정을 직접 구현하니 뭔가 그 동안 악당으로 알고 지내던 존재의 유년 시절을 경험한 듯한 색다른 경험을 하게 된 것 같습니다.

이젠 내가 데이터를 주는 입장이 되니 뭔가 감회가 새롭네요…! 이래서 장고를 배우길 잘했다 싶기도 하고…!! ㅎㅎ

실제 사용자에게 데이터를 제공해주려면 보안 상에 문제가 없을 수준에서 모델을 공개해주란 말이야….

참고하면 좋은 글

Nested serialization 관련 공식 문서

Leave a Comment

목차