DRF-단일 모델조회 API와 외래키

김의석 ·2024년 6월 4일

Django

목록 보기
29/39
post-thumbnail

드.디.어 궁금했던 DRF에 대해 학습한다! (‘•̀ ▽ •́ )
이제 react와 연결할 수 있는거야?

DRY 철학

매번 반복되는 코드의 REST API 개발 패턴을 APIView 클래스 기반 뷰를 통한 중복제거에 초점

다양한 포맷의 요청 처리 및 다양한 포맷의 응답 지원

API 요청에 대한 유효성 검증

다양한 인증 방법 및 권한 제공

직렬화란?

단일 모델 조회 API 만들기

1) urls.py setting

실습을 위한 urls.py setting으로 내용은 아래와 같다!


# project/urls.py
path("blog/", include("blog.urls")),  # drf test

# app/urls.py
urlpatterns = []

urlpattern_api_v1 = [
    path("", api.post_list, name="post_list"),
    path("<int:pk>", api.post_detail, name="post_detail"),
]

urlpatterns += [
    path("api/", include((urlpattern_api_v1, "api-v1"))),
]

실습으로 처음 접해본 urlpatterns의 라우팅 형태,,! API version 별 라우팅을 위한 것이라고 한다! (‘•̀ ▽ •́ )

2) serializers.py 작성

djnag에서 model object의 직렬화를 위해 serializers.py를 작성해야한다!

# serializers.py

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        
        # fields = '__all__' 모든 필드를 불러오는 경우
        fields = [
            "id",
            "title",
            "content",
        ]

3) serializers.ModelSerializer

PostSerializer는 serializers.ModelSerializer를 상속 받는다.
serializers.ModelSerializer는 django의 모델자동으로 매핑되는 직렬화 클래스를 쉽게 생성해 준다.
그 밖에 serializers.ModelSerializer의 주석에는 이러한 설명이 잘 작성되어있는데 일부만 가져와 보았다!

"""
A `ModelSerializer` is just a regular `Serializer`, except that:

* A set of default fields are automatically populated.
* A set of default validators are automatically populated.
* Default `.create()` and `.update()` implementations are provided.

The process of automatically determining a set of serializer fields
based on the model fields is reasonably complex, but you almost certainly
don't need to dig into the implementation.
"""

GPT의 해석
ModelSerializer는 기본적으로 Serializer와 같지만 다음과 같은 추가 기능을 제공합니다:

기본 필드 자동 생성: 모델 필드를 기반으로 직렬화 필드가 자동으로 생성됩니다.
기본 유효성 검사기 자동 생성: 모델의 제약 조건에 맞춰 유효성 검사기가 자동으로 설정됩니다.
기본 create() 및 update() 메서드 제공: 인스턴스를 생성하고 업데이트하는 기본 구현이 제공됩니다.
모델 필드를 기반으로 직렬화 필드를 자동으로 결정하는 과정은 꽤 복잡하지만, 일반적으로 구현 세부 사항을 알 필요는 없습니다.

# rest_framework/serializers.py

class ModelSerializer(Serializer):
		serializer_field_mapping = {
    	models.AutoField: IntegerField,
        models.BigIntegerField: IntegerField,
        models.BooleanField: BooleanField,
        # 등등등 매핑을 위한 다양한 모델 필드가 존재!
    }
    def create(self, validated_data):
    def update(self, instance, validated_data):
    def get_fields(self):
    def get_field_names(self, declared_fields, info):
    
    # 등등등 어마어마하게 많은 메서드가 존재하고 있다!

요즘 각 모듈을 뜯어보면서'내가 이것들을 다 써볼 수 있을까?'라고 느낀다.
어서 학습해서 다 써봐야지! ദി◍•ᴗ•◍)

4) list/detail 조회 시에 '같은' 데이터 구조

실습에서는 api.py 파일을 생성하여 아래 두가지 메서드를 작성했다!

(1) list 조회 API

from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework.utils.serializer_helpers import ReturnList, ReturnDict
from blog.models import Post
from blog.serializers import PostSerializer

def post_list(request: HttpRequest) -> HttpResponse:
    post_qs = Post.objects.all()

    serializer = PostSerializer(post_qs, many=True)
    list_data: ReturnList = serializer.data
    # ReturnList을 지정해야 list_data에 serializer.data가 담긴다.
    print(serializer.data)

    return JsonResponse(list_data, safe=False)

Post모델의 모든 object를 조회하는 메서드

serializer = PostListSerializer(post_qs)에서 반드시 many=True를 지정!

  • N+1 가져올 수 있는 문제를 방지한다. serializer에서 지정한 필드만 가져오기 때문!
  • N+1 문제란? 기본 쿼리 1개와 관련된 객체마다 추가 쿼리 N개가 발생하여 성능 저하를 초래하는 문제!

list_data: ReturnList = serializer.data

  • serializer.data는 DRF의 ListSerializer에서 반환된 결과로, ReturnList 타입의 데이터를 반환!
  • 타입 힌팅(list_data: ReturnList)은 코드의 가독성과 안정성을 높이는 데 도움!
  • ReturnList는 DRF에서 사용되는 커스텀 리스트 타입으로, 일반 리스트처럼 동작하면서 추가적인 메타 데이터를 포함할 수 있다!

(2) detail 조회 API

from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework.utils.serializer_helpers import ReturnList, ReturnDict
from blog.models import Post
from blog.serializers import PostSerializer

def post_detail(request: HttpRequest, pk) -> HttpResponse:
    post_qs = Post.objects.all()
    post = get_object_or_404(post_qs, pk=pk)

    serializer = PostSerializer(instance=post)
    detail_data: ReturnDict = serializer.data

Post모델의 특정 object를 조회하는 메서드

5) list/detail 조회 시에 '다른' 데이터 구조

앞서 list/detail메서드에서 필요한 쿼리셋이 다른 경우의 구현이다!

(1) serializers.py의 수정

class PostListSerializer(serializers.ModelSerializer):

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
        ]


class PostDictSerializer(serializers.ModelSerializer):

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "content",
        ]

각각 필요한 직렬화 클래스를 작성, 그리고 쿼리셋에 맞게 필드를 수정해준다.(content의 차이)

(2) list 조회 API

from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework.utils.serializer_helpers import ReturnList, ReturnDict
from blog.models import Post
from blog.serializers import PostSerializer

def post_list(request: HttpRequest) -> HttpResponse:
    post_qs = Post.objects.all().defer("content")

    serializer = PostSerializer(post_qs, many=True)
    list_data: ReturnList = serializer.data
    print(serializer.data)

    return JsonResponse(list_data, safe=False)

post_qs = Post.objects.all().defer("content")

  • 조회시 필드에서 제외할 수 있도록 defer("content")를 설정!

(3) detail 조회 API

from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework.utils.serializer_helpers import ReturnList, ReturnDict
from blog.models import Post
from blog.serializers import PostSerializer

def post_detail(request: HttpRequest, pk) -> HttpResponse:
    post_qs = Post.objects.all()
    post = get_object_or_404(post_qs, pk=pk)

    serializer = PostSerializer(instance=post)
    detail_data: ReturnDict = serializer.data

외래키 관계 조회

1) pk 필드(id)

(1) serializers.py의 수정

class PostListSerializer(serializers.ModelSerializer):

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "author",
        ]


class PostDictSerializer(serializers.ModelSerializer):

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "content",
            "author",
        ]

Post 모델의 필드 author는 user모델을 참조하는 외래키이다!

  • fields에 지정하는 것만으로 기본으로 설정되어 조회된다.

(2) list 조회 API

post_qs = Post.objects.all().defer("content").select_related("author")

objects.all()로 모든 객체를 가져오는 list 조회 API에서만 .select_related("author")을 추가하여 N+1문제가 발생하지 않도록 한다.

2) 메서드를 사용하지 않는 pk 필드(id)의 문자열 표현

class PostListSerializer(serializers.ModelSerializer):
    author = serializers.CharField(source="author.username")

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "author",
        ]


class PostDictSerializer(serializers.ModelSerializer):
    author = serializers.CharField(source="author.username")

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "content",
            "author",
        ]

author = serializers.CharField(source="author.username")로 외래키의 문자열 표현이 가능하다!

3) 중첩된 사전(=중첩 Serializer)

외래키로 참조하는 모델의 다른 필드를 Serializer를 중첩하여 조회한다.
serializers.py에 중첩하여 조회할 외래키 필드에 대한 직렬화 클래스를 작성!

(1) AuthorSerializer 클래스 생성!

class AuthorSerializer(serializers.ModelSerializer):
    name = serializers.SerializerMethodField()

    def get_name(self, customuser) -> str:
        return f"{customuser.first_name} {customuser.last_name}"

    class Meta:
        model = CustomUser
        fields = [
            "id",
            "email",
            "name",
            "username",
        ]

SerializerMethodField는 모델에 존재하지 않는 사용자 정의 필드를 직렬화할 때 사용한다.

get_name메서드는 user모델에 없는 사용자 정의필드를 생성한다! 이 메서드는 SerializerMethodField 내부 bind 메서드를 통해서 바인딩 된다!
여기서는 first_name+last_name = name 필드를 새롭게 생성!

(2) 중첩 Serializer 적용하기

class PostListSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()
    
    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "author",
        ]

중첩을 적용할 serializer 클래스에 author = AuthorSerializer()으로 직렬화 인스턴스를 생성하면 끝!

AuthorSerializer의 fields에 작성한 내용들이 반영 됨을 확인!

4) 외래키에 대한 역참조, Post(1)->Comment(N)

(1) 역참조(기본), Commemt 모델의 message만 조회

class PostDetailSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()

    comment_list = serializers.StringRelatedField(many=True, source="comment_set")
    
        class Meta:
        model = Post
        fields = ["id", "title", "content", "author", "comment_list"]

StringRelatedField 참조 필드의 객체를 문자열로 직렬화한다.
참조할 모델(이 경우 comment)의 __str__ 메서드의 결과를 직렬화할 때 유용하다.

StringRelatedField의 속성은 다음과 같다.

  • many=True: 여기서 하나의 Post와 관련된 여러 개의 Comment 객체를 가져오는 역할을 한다! 즉, 1:N에서 여러 관련 객체를 직렬화할 때 사용된다.

  • source: 직렬화할 필드를 지정. source="comment_set"는 Comment 모델의 역참조 이름인 comment_set을 사용!

(2) 역참조(CommentSerializer), Commemt 모델의 message+ id필드 조회

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment

        fields = [
            "id",
            "message",
        ]

Comment 모델 내에 다른 필드도 참조하기 위해 직렬화 클래스를 작성한다!

class PostDetailSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()

    comment_list = CommentSerializer(
        many=True, source="comment_set"
    ) 
    
        class Meta:
        model = Post
        fields = ["id", "title", "content", "author", "comment_list"]

comment_list = CommentSerializer(many=True, source="comment_set")로 인스턴스를 생성, 앞서 중첩 Serializer 방법과 동일하다!

commet_list에 CommentSerializer에서 작성한 id, message필드가 직렬화 된 것을 확인할 수 있다!

DRF 뷰 래핑

DRF 뷰 래핑은 아래와 같은 이점이 있다.

잘 이해가 안가서 GPT에게 각 키워드에 대한 설명을 부탁했다,,⎝ᑒ⎠

1) JSON요청 지원, 요청에 맞는 응답포맷으로 응답

JSON 요청: Django REST Framework는 클라이언트로부터의 HTTP 요청을 JSON 형식으로 처리할 수 있습니다. 이는 HTTP 요청의 본문(body)에 JSON 데이터를 포함하여 전송하는 방식입니다.

응답 포맷: Django REST Framework는 기본적으로 JSON 형식으로 응답을 생성합니다. 이는 API의 응답 데이터를 JSON으로 직렬화하여 클라이언트에게 반환하는 것을 의미합니다.

2) 여러 인증 지원(세션인증, 기본인증)

세션 인증: Django REST Framework는 Django의 세션 기반 인증을 지원합니다. 이는 사용자의 세션 정보를 기반으로 사용자를 인증하는 방식입니다.

기본 인증: HTTP 기본 인증(Basic Authentication)을 지원합니다. 클라이언트는 요청을 보낼 때 인증 정보를 Authorization 헤더에 담아서 서버에 전달하고, 서버는 해당 인증 정보를 확인하여 인증합니다.

3) 요청 권한 체크

Django REST Framework는 요청을 받은 후 해당 요청에 대한 인증과 권한을 확인합니다. 이는 각각 요청을 보낸 사용자가 누구인지 확인하고, 해당 사용자가 요청한 동작을 수행할 수 있는 권한이 있는지 확인하는 것을 의미합니다.

인증(authentication)과 권한(permissions)은 Django REST Framework에서 사용자의 접근을 제어하는 중요한 요소입니다.

4) 호출횟수 허용량 초과 체크 지원

Django REST Framework는 요청을 제한하고 호출 횟수를 제한하는 기능을 지원합니다. 이는 요청을 받은 후 해당 요청의 헤더에 있는 사용자의 식별 정보를 기반으로 호출 횟수를 확인하고, 허용된 호출 횟수를 초과하는 경우 적절한 응답을 반환합니다.

이러한 호출 횟수 제한 기능은 API 서비스를 안정적으로 운영하기 위해 중요합니다. 무분별한 호출 횟수로 인한 서버 부하를 방지하고, 사용자 경험을 향상시키는 데 도움이 됩니다.

1) list API

decoreators를 구현

@api_view(["GET"])
def post_list(request: Request) -> Response:
    post_qs = Post.objects.all().defer("content").select_related("author")

    serializer = PostListSerializer(post_qs, many=True)
    list_data: ReturnList = serializer.data
    print(serializer.data)

    return Response(list_data)

HttpRequest ->HttpResponse가 아닌 post_list(request: Request) -> Response:로 수정하여 DRF에서 요청-응답을 처리할 수 있게한다!

DRF APIView 클래스 기반 구현

class PostListAPIView(ListAPIView):
    queryset = PostListSerializer.get_optimized_queryset()
    serializer_class = PostListSerializer
    
post_list_view = PostListAPIView.as_view()

ListAPIView를 조금 더 알아보자!(‘•̀ ▽ •́ )

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

이 클래스를 상속받은 경우, HTTP GET 요청을 처리하여 데이터 리스트를 반환하는 역할을 한다. 즉 데이터베이스에서 데이터를 조회하고 이를 직렬화하는 것.

  • mixins.ListModelMixin = ListAPIView의 return list()
    list() 메서드를 통해 데이터를 조회하고 이를 클라이언트에게 반환할 수 있다!

  • GenericAPIView = ListAPIView의 get()
    Generic API 뷰의 기본 틀을 제공하며, HTTP 요청을 처리하는 메서드(get(), post(), put(), delete() 등)를 구현하여 Generic API를 만들 수 있다!

2) detail API

decoreators를 구현

@api_view(["GET"])
def post_detail(request: Request, pk) -> Response:
    post_qs = Post.objects.all()
    post = get_object_or_404(post_qs, pk=pk)
    serializer = PostDictSerializer(instance=post)
    detail_data: ReturnDict = serializer.data

    return Response(detail_data)

DRF APIView 클래스 기반 구현

class PostRetrieveAPIView(RetrieveAPIView):
    queryset = PostListSerializer.get_optimized_queryset()
    serializer_class = PostDetailSerializer

post_detail_view = PostRetrieveAPIView.as_view()

RetrieveAPIView를 ✎ (๑╹o╹)✎ 알아보자!

class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    """
    Concrete view for retrieving a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

이 클래스는 단일 객체에 대한 조회를 처리하는 뷰이다!

  • RetrieveModelMixin: 단일 객체에 대한 조회를 위한 기능을 제공하는 믹스인(mixin) 클래스. retrieve() 메서드를 제공하여 단일 객체의 조회를 수행!

  • GenericAPIView: 앞서 ListAPIView의 사용목적과 동일!

가독성과 유지보수 향상을 위한 QuerySet과 Serializer

지금까지 실습에서는 serializer.py에서는 직렬화를 api.py에서는 QuerySet을 따로 작성하였다.
하지만 가독성과 유지보수 향상을 위해 다음과 같이 코드를 정리 할 수있다.

1) serializer.py

각 직렬화 클래스 내부에 @staticmethod를 다음과 같이 추가한다!

list API

class PostListSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()

    class Meta:
        model = Post
        fields = [
            "id",
            "title",
            "author",
        ]

    @staticmethod
    def get_optimized_queryset() -> QuerySet[Post]:
        return Post.objects.all().only("id", "title", "author").select_related("author")

이때 기존의 QuerySet에서 사용하던 defer를 제외하고 only를 사용하여 쿼리셋과 직렬화 코드의 필드를 일치 시킨다.
쿼리셋과 직렬화 코드의 의미를 동일하게 가져가는 것이 좋다!

detail API

class PostDetailSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()

    comment_list = CommentSerializer(many=True, source="comment_set")

    @staticmethod
    def get_optimized_queryset() -> QuerySet[Post]:
        return Post.objects.all()

    class Meta:
        model = Post
        fields = ["id", "title", "content", "author", "comment_list"]

2) api.py

뷰 클래스의 queryset을 PostListSerializer.get_optimized_queryset()로 수정!

list API

class PostListAPIView(ListAPIView):
    queryset = PostListSerializer.get_optimized_queryset()
    serializer_class = PostListSerializer

detail API

class PostRetrieveAPIView(RetrieveAPIView):
    queryset = PostListSerializer.get_optimized_queryset()
    serializer_class = PostDetailSerializer

이렇게함으로 serializer.py 안에서 QuerySet과 Serializer의 유지보수가 가능해졌다!

응답 데이터 구조 변경

JSON 응답 한해서만 응답 데이터 구조를 변경해보자

api.py

list API

def list(self, request: Request, *args, **kwargs):
    response: Response = super().list(request, *args, **kwargs)

    if isinstance(request.accepted_renderer, (JSONRenderer, BrowsableAPIRenderer)):
        response.data = ReturnDict(
            {
                "ok": True,
                "result": response.data,  # ReturnList
            },
            serializer=response.data.serializer,  # response.data.serializer : ReturnDict이 serializer 속성을 지원
        )
    return response

응답데이터 구조를 변경하는 과정은 이렇다! ╰( ▪‿▪)╮

① list()는 ListAPIView에서 상속받은 것을 오버라이드한 것이며 API 엔드포인트에서 GET 요청을 받았을 때 실행한다!

super().list()를 호출하여 request에 대한 기본적인 데이터 조회 및 직렬화를 수행하고, 이를 response 변수에 할당한다!

request.accepted_renderer는 클라이언트가 요청한 렌더러를 의미. 여기서는 JSON 렌더러 또는 Browsable API 렌더러를 확인

④ 응답 데이터의 구조를 변경 시에는 새로운 딕셔너리 형태로 재정의한다. 이때, 기존의 응답 데이터는 result키에 위치하고, 이를 "ok": True와 함께 감싸는 형태로 변경!

⑤ serializer=response.data.serializer은 ReturnDict의 serializer은 속성 지원이다!

detail API

list API와 과정은 동일하고 단 retrieve 메서드를 재정의하고 retrieve()를 오버라이드하는 것이 차이!

응답 데이터의 구조가 변경 됨을 확인할 수 있다!

끝!

profile
널리 이롭게

0개의 댓글