장고 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 관계 모델을 계층형 데이터로 응답할 수 있게 됩니다.
(오른쪽) 상세 요청에서는 Nested 관계를 계층 구조 JSON 으로 응답
계층 구조 JSON 장점
위에서도 언급했지만 Nested 관계를 만들어 계층 구조로 응답하면 아래와 같은 장점을 가져갈 수 있습니다.
향상된 데이터 표현
- Nested 관계를 통해 계층적 데이터 구조를 보다 자연스럽게 표현할 수 있습니다.
복잡성 감소
- 상위 개체 내에 관련 개체를 캡슐화함으로써 중첩 관계는 데이터 조작을 단순화하고 중복성을 줄이는 데 도움이 됩니다.
마치며
저는 업무상 서버에서 “DB 데이터 -> 직렬화 -> 응답” 하는 과정보다는 클라이언트 측에서 “JSON -> 데이터 모델링 -> DB에 load” 하는 과정에 더 익숙했습니다.
API 제공 측에서 DB 모델을 공개하지 않는 이상 복잡한 계층의 JSON 데이터를 모델링하는 일은 쉽지 않았는데요…
백엔드에서 계층 구조로 직렬화하여 응답해주는 과정을 직접 구현하니 뭔가 그 동안 악당으로 알고 지내던 존재의 유년 시절을 경험한 듯한 색다른 경험을 하게 된 것 같습니다.
이젠 내가 데이터를 주는 입장이 되니 뭔가 감회가 새롭네요…! 이래서 장고를 배우길 잘했다 싶기도 하고…!! ㅎㅎ
실제 사용자에게 데이터를 제공해주려면 보안 상에 문제가 없을 수준에서 모델을 공개해주란 말이야….