[Django] GET API 분석, APIView

서재환·2022년 8월 30일
0

Django

목록 보기
24/40

class 분석

지금부터 APIView로 작성된 짧은 GET API에 대한 분석을 할 것이다. 기본이 되는 API 요청이지만 코드 한 줄 한 줄 읽어가면서 그 의미를 짚어볼 것이므로 평소 그냥 가볍게 인지하고 지나쳤던 분들에게 유용하면 좋겠다.

from rest_framework import permissions, status
from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema


class MyInfoView(APIView):
    """
    내 정보 조회
    """

    permission_classes = (permissions.IsAuthenticated,)

    @swagger_auto_schema(responses={200: MemberSerializer}) #1
    def get(self, request):
        member = request.user #2
        res = MemberSerializer(member) #3
        return Response(res.data, status=status.HTTP_200_OK) #4

swagger

swagger_auto_schema
해당 함수는 데코레이터로 쓰이는 함수이다. 내부 코드를 보면 decorator 함수가 있고 swagger_auto_schema 함수는 decorator 함수를 반환한다.

편의를 위해 많은 인자를 *args로 적었다.

def swagger_auto_schema(*args, **kwargs):
	def decorator(view_method):
    	...
        중략
        ...
    return decorator

아래와 같이 class 내부 get 함수 위에 decorator를 붙임으로 해서 domain/swagger url에서 domain/api/member/my로 api를 호출하면 swagger page 아래 그림의 Description 부문에 model MemberSerializer의 형식이 들어오게된다.(사용자와 관련된 정보가 많아 일부만을 잘라 캡처하였다)

class MyInfoView(APIView):
  @swagger_auto_schema(responses={200: MemberSerializer})
  def get(self, request):
      ...

swagger 매개변수 responses에 딕셔너리 형태로 넣는다. 이 때 2XX 형태의 반응이 올 경우 작성자가 기입한 상태코드와 값이 스웨거문서에 기록되는데 2XX 형태의 반응이 오지 않을 경우 서버에서 넘겨준 정보인 Response body와 Response header 부문을 보낸다. 예시 그림은 아래와 같다. 해당 오류는 API 요청시 JWT 토큰이 없어 발생한 오류이다.

인자 및 변수 알기

앞으로 넘어가기에 앞서 해당 api에 사용되는 매개인자 및 변수에 대해 그 정체를 미리 파악하면 후에 코드를 보는데 도움이 된다. print로 찍어서 확인한 부문이다. 한번 확인 한 후에 넘어가도록 하자.

self ---> app.member.views.MyInfoView
request ---> rest_framework.request.Request
memer ---> apps.member.models.Member
res ---> app.member.serializers.MemberSerializer
Response ---> rest_framework.response.Response

Framework 흐름 관점에서 이해하기

아래 그림은 웹프레임워크인 Django가 사용자의 요청을 처리하는 순서 과정을 도식화한 그림이다.
간단하게만 살펴보자. URL은 url요청을 VIEW로 전달한다. VIEW는 로직을 처리하는 부문이다. MODEL은 디비 관련해서 처리 할게 있으면 쓰이는 부문이고 TEMPLATE은 화면에 렌더링해줄 부문이 있을 때 쓰이는 부문이다.

URL

클라이언트로부터 서버에 request가 전달되면, URLconf(urls.py)를 이용하여 해당 파일에 기입된 URL을 파싱 후 매칭한다. 매칭했다는 것은 해당 요청을 처리한 로직을 VIEW에서 발견했다는 것이다.

여기서 path 함수의 name 인자는 해당 api를 명명 하는 역할로 사용된다. name을 지정 할 경우 reverse, redirect 함수 그리고 template을 사용할 때 URL을 하드코딩 하지 않아도 되는 이점이 있고 urls.py 만 관리하면 되기 때문에 name을 지정하는 것이 용이하다.

클래스 뷰를 사용한다면 클래스 이름 뒤에 .as_view()를 붙여서 사용하면 된다.

urlpatterns = [
  path("member/my", views.MyInfoView.as_view(), name="myinfo_view")
  .
  .
  ...

View

View는 자신의 로직을 실행하면서 데이터 베이스의 처리가 필요하면 Model및 Template과 상호작용한다.

APIView (공문)

해당 객체가 상속하는 APIView는 일반 View를 상속받은 객체와는 다소 다르다. 공식문서에서 여러가지 예를 통해 서로 비슷하지만 다른 두 객체를 설명하고있다.
DRF APIView 상세

클래스 뷰 작성시 reqeust 인자의 정체

인자로 MyInfoView의 인스턴스인 selfrequest 인자를 받아서 로직을 수행한다. request 인자가 그냥 변수가 아닌 것은 최하위 View 클래스에서 request 변수에 HttpRequest 객체를 받았기 때문이다. 그리고 해당 변수는 initialize_request 메서드에 인자로 들어가고 해당 함수는 Request 객체를 반환한다. 그 반환한 객체에 이와 다른 request 인스턴스를 넣어 반환한다.

class MyInfoView의 상속 관계

[ <class 'apps.member.views.MyInfoView'>, 
  <class 'rest_framework.views.APIView'>, 
  <class 'django.views.generic.base.View'>, 
  <class 'object'> ]
class View:
    http_method_names: List[str] = ...
    request: HttpRequest = ...
class APIVIew(View):
    def initialize_request(self, request, *args, **kwargs):

        return Request(
            request,
    )

	def dispatch(self, request, *args, **kwargs):
    	request = self.initialize_request(request, *args, **kwargs)

Request

class Request:
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.

    Kwargs:
        - request(HttpRequest). The original request instance.
    	...
    """

해당 API로 요청이 들어온 후 request.user 해당 부문은 property로 처리한다.

프러퍼티 user 기능은 아래 설명에 나와 있듯이 현 api 요청을 한 유저를 반환하는데 인증관련 로직을 처리한 후 반환한다.

class Request:
... 중략 ...

    @property
    def user(self):
        """
        Returns the user associated with the current request, as authenticated
        by the authentication classes provided to the request.
        """
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user

    @user.setter
    def user(self, value):
        """
        Sets the user on the current request. This is necessary to maintain
        compatibility with django.contrib.auth where the user property is
        set in the login and logout functions.

        Note that we also set the user on Django's underlying `HttpRequest`
        instance, ensuring that it is available to any middleware in the stack.
        """
        self._user = value
        self._request.user = value

... 중략 ...

인스턴스 self의 속성 _user의 경우 Request 객체에서 생성자 함수에 그 값이 없는 것으로 확인된다. 따라서 user 프로퍼티에서 self._user가 생성된 후 반환된다. request.user는 self._user의 값을 담고 있다.

request.user = self._user 이다.

self._user의 반환 값의 경우 아래의 로직이 처리된 이후 self._user가 반환된다.

if not hasattr(self, '_user'):
    with wrap_attributeerrors():
        self._authenticate()
    return self._user

@wraps(func)

인수가 있는 데코레이터

인수가 있는 데코레이터를 만드는 이유는 매개변수를 넣기 위함이다.

참고링크

Model

MemberSerializer(member) 분석

MemberSerializer (ModelSerializer < Serializer < BaseSerializer)

MemberSerializer는 최하단의 BaseSerialzer를 상속 받는다.

Serializer 안에 property로 쓰이는 data가 존재하는데 res.data를 찍는 순간 프로퍼티로 지정한 data 메서드가 실행된다. 해당 메서드는 ReturnDict(ret, serializer=self)의 인스턴스를 반환한다.

그 전에 super().data로 부모클래스의 프로퍼티 메서드 data에 접근하한다. 해당 메서드는 self_.data에 값을 넣어주는 기능을 수행한다. 해당 메서드(#3) 안에는 self.instance 부문이 존재하는데 해당 부문은 아래에서 다루도록 하자.

#1
def get(self, request):
    member = request.user #2
    res = MemberSerializer(member) #3
    return Response(res.data, status=status.HTTP_200_OK) #4

memeber 값의 경우 데이터베이스 member model의 id와 name에 들어간 값이다. id는 기본키에 해당하고 name의 경우 field에 해당한다.

print(member) #[admin_super] 관리자
print(type(member)) #<class 'apps.member.models.Member'>
#2
@property
def data(self):
	ret = super().data
	return ReturnDict(ret, serializer=self)
#3
@property
def data(self):
    if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
        msg = (
            'When a serializer is passed a `data` keyword argument you '
            'must call `.is_valid()` before attempting to access the '
            'serialized `.data` representation.\n'
            'You should either call `.is_valid()` first, '
            'or access `.initial_data` instead.'
        )
        raise AssertionError(msg)

    if not hasattr(self, '_data'):
        if self.instance is not None and not getattr(self, '_errors', None):
            self._data = self.to_representation(self.instance)
        elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
            self._data = self.to_representation(self.validated_data)
        else:
            self._data = self.get_initial()
    return self._data

MemberSerialzer(member)는 BaseSerializer를 상속 받는다고 했다. 해당 class는 매개인자로 instance를 받는데 아래 구문을 실행할 때 BaseSerializer에 Member의 인스턴스가 들어가게 된다.

그래서 res.data를 찍는 순간 Serialzer의 data 메서드가 실행되고 그 메서드 안에서 BaseSerializer의 data 메서드가 실행되게 되는데 그 data 메서드 안에 Member의 인스턴스 self.instance가 들어가게 되면서 최종적으로 self._data를 반환하고 해당 값이 ret형태로 ReturnDict에 들어가게 되는 것이다.

그래서 res.data 안에 Member의 데이터가 들어갈 수 있게 되는 것이다.

res = MemberSerializer(member)
#1
class BaseSerializer(Field):
    ...
    
    def __init__(self, instance=None, data=empty, **kwargs):
        self.instance = instance
        if data is not empty:
            self.initial_data = data
        self.partial = kwargs.pop('partial', False)
        self._context = kwargs.pop('context', {})
        kwargs.pop('many', None)
        super().__init__(**kwargs)
    ...
#2
@property
def data(self):
	ret = super().data
	return ReturnDict(ret, serializer=self)
#3
@property
def data(self):
    if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
        msg = (
            'When a serializer is passed a `data` keyword argument you '
            'must call `.is_valid()` before attempting to access the '
            'serialized `.data` representation.\n'
            'You should either call `.is_valid()` first, '
            'or access `.initial_data` instead.'
        )
        raise AssertionError(msg)

    if not hasattr(self, '_data'):
        if self.instance is not None and not getattr(self, '_errors', None):
            self._data = self.to_representation(self.instance)
        elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
            self._data = self.to_representation(self.validated_data)
        else:
            self._data = self.get_initial()
    return self._data

res.data의 return 값은 ReturnDict(ret, serializer=self)이다. ReturnDict도 클래스이기 때문에 결국은 해당 클래스의 인스턴스를 반환한다.

Response 인스턴스는 api를 요청한 유저의 데이터와 응답 코드에 대한 정보를 갖고 있는 인스턴스이며 해당 객체의 역할은 data=value 첫번째 데이터로 들어온 값을 기반으로 추가적인 metadata를 뽑아내는 역할을 수행하는 것 같다.

class Response(SimpleTemplateResponse):
    """
    An HttpResponse that allows its data to be rendered into
    arbitrary media types.
    """

  def __init__(self, data=None, status=None,
             template_name=None, headers=None,
             exception=False, content_type=None):
  ...

Template

View 및 Model 로직의 처리가 완료되면, Template과 함께 클라이언트로 반환된다.

클라이언트는 이 response를 전달받아 화면에 최종적인 결과물을 사용자에게 제공한다.

정리

class MyInfoView(APIView):
    """
    내 정보 조회
    """

    permission_classes = (permissions.IsAuthenticated,)

    @swagger_auto_schema(responses={200: MemberSerializer}) #1
    def get(self, request):
        member = request.user #2
        res = MemberSerializer(member) #3
        return Response(res.data, status=status.HTTP_200_OK) #4

해당 URL로 요청이 들어왔을 때 domain/api/member/my 응답을 주는 로직에 대해서 하나하나 살펴보았다.

  • 스웨거 코드는 스웨거 문서에 해당 api를 호출 후 정상 작동 했을 때 상태코드응답받은 데이터 양식을 표시해주는 역할을 한다.
  • 변수 memeber는 해당 api를 요청한 사용자의의 모델인 member의 인스턴스를 담고있다.
  • 변수 res는 직렬변환기에 인스턴스를 member를 인자로 하는 인스턴스를 담고 있다.
  • 반환 값은 객체 Response에 json과 상태코드를 인자로 하는 인스턴스를 반환하고 있다.

간단한 GET 요청이지만 조금 상세하게 살펴 보았다. GET 요청을 할 때 위와 같은 구문이 예시로 쓰인다는 것을 아는 것도 중요하겠지만 매개인자와 반환 값 그리고 객체를 정확히 알고 위 구문을 이해한다면 분명 다른 코드를 이해하는데도 도움이 될 것이다.

또한 그 안에 쓰인 상속, 데코레이터, 프로퍼티, 시리얼라이저에 대한 개념을 이해한다면 더욱 도움이 될 것이라고 생각한다.

0개의 댓글