[Django] GET API 분석, APIView

서재환·2022년 8월 31일
0

Django

목록 보기
25/40

class 분석

class ExistsIdView(APIView):
    """
    아이디 중복 체크

    ---
    """

    permission_classes = (permissions.AllowAny,)

    id_param = openapi.Parameter(
        "id", openapi.IN_PATH, description="user id", type=openapi.TYPE_STRING
    )

    @swagger_auto_schema(
        manual_parameters=[id_param], responses={200: BooleanResponseSerializer}
    )
    def get(self, request, id):
        try:
            member = Member.objects.get(pk=id)
        except Member.DoesNotExist:
            raise exceptions.NotFound("member not found")

        res = BooleanResponseSerializer(data={"result": True})
        if res.is_valid():
            return Response(res.data)

views.ExistsIdView.as_view()

path("member/<str:id>/exists",
	views.ExistsIdView.as_view(), name="exists_id_view")

클래스 메서드인 as_view(cls, initkwargs)가 실행 될 때 내부 코드중 아래를 눈여겨 보아야한다.

super().as_view(**initkwargs)

해당 코드가 실행되면서 as_view() 함수가 실행된다.

첫번 째 as_view(cls, initkwargs) 함수는 @classmethod 이고 두번 째 as_view(**initkwargs) 함수는 @classonlymethod 이다.

참고 링크
둘 간의 차이는 전자의 경우 인스턴스도 as_view 함수에 접근할 수 있지만 후자의 경우 클래스만 as_view 함수에 접근할 수 있다.

첫번 째 as_view(cls, initkwargs) 메서드의 경우 APIView(View) 클래스 내의 클래스 메서드이므로 super()의 경우 class View에 해당한다.

참고 링크
첫번 째 as_view(cls, **initkwargs) 함수는 csrf_exempt(view)를 return 하는데 csrf_exempt 함수는 인자로 받는 함수에게 csrf(Cross Site Request Forgery) 관련 token이 불필요 하다는 처리 기능을 담당한다.

@classmethod as_view

위 함수가 처리하는 주요 기능은 View.as_view() 함수를 호출한 결과 값인 view 함수를 변수 view에 할당하는 것이다. view 함수는 self.dispatch(request, *args, **kwargs)의 결과값을 리턴한다. dispatch 함수는 HttpResponse를 리턴한다.

 @classmethod
    def as_view(cls, **initkwargs):
        """
        Store the original class on the view function.

        This allows us to discover information about the view when we do URL
        reverse lookups.  Used for breadcrumb generation.
        """
        if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
            def force_evaluation():
                raise RuntimeError(
                    'Do not evaluate the `.queryset` attribute directly, '
                    'as the result will be cached and reused between requests. '
                    'Use `.all()` or call `.get_queryset()` instead.'
                )
            cls.queryset._fetch_all = force_evaluation

        view = super().as_view(**initkwargs)
        view.cls = cls
        view.initkwargs = initkwargs

        # Note: session based authentication is explicitly CSRF validated,
        # all other authentication is CSRF exempt.
        return csrf_exempt(view)

classonlymethod as_view

@classonlymethod
def as_view(cls, **initkwargs):
    """Main entry point for a request-response process."""
    for key in initkwargs:
        if key in cls.http_method_names:
            raise TypeError(
                'The method name %s is not accepted as a keyword argument '
                'to %s().' % (key, cls.__name__)
            )
        if not hasattr(cls, key):
            raise TypeError("%s() received an invalid keyword %r. as_view "
                            "only accepts arguments that are already "
                            "attributes of the class." % (cls.__name__, key))

    def view(request, *args, **kwargs):
        self = cls(**initkwargs)
        self.setup(request, *args, **kwargs)
        if not hasattr(self, 'request'):
            raise AttributeError(
                "%s instance has no 'request' attribute. Did you override "
                "setup() and forget to call super()?" % cls.__name__
            )
        return self.dispatch(request, *args, **kwargs)
    view.view_class = cls
    view.view_initkwargs = initkwargs

    # take name and docstring from class
    update_wrapper(view, cls, updated=())

    # and possible attributes set by decorators
    # like csrf_exempt from dispatch
    update_wrapper(view, cls.dispatch, assigned=())
    return view

dispatch 함수

dispatch 함수는 HTTP Response를 리턴한다.

    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

정리

path 함수 내 views.ExistsIdView.as_view()의 실행은 as_view(cls, **initkwargs) 함수의 호출로 view 함수를 리턴하고 해당 함수가 path 함수의 인자로 들어간다. 그 과정에서 ¹ csrf 토큰의 무효처리와 ² HttpResponse를 처리하는 것을 볼 수 있었다.

class Parameter

참고링크

@swagger_auto_schema 중 manual_parameters 인자에 대해서 알아보고자 한다.

해당 인자는 요청 시 추가 정보를 더함으로써 응답을 커스터마이징 하는 역할을 수행한다. 리스트 형태로 여러개를 담을 수 있으며 Class Parameter의 인스턴스 객체를 리스트 안에 담는다.

해당 api의 id_param 인스턴스의 경우 경로에 대한 정보를 제공해주는 역할을 한다. swagger 문서에선 Parameters 부문에 아래 그림과 같이 기입된다.

id_param = openapi.Parameter("id", openapi.IN_PATH,
	description="user id", type=openapi.TYPE_STRING)
@swagger_auto_schema(
    manual_parameters=[id_param], 
    responses={200: BooleanResponseSerializer}
)
class Parameter(SwaggerDict):
    def __init__(self, name, in_, description=None, required=None, 
    	schema=None, type=None, format=None, enum=None, pattern=None, 
        items=None, default=None, **extra):
        ...
name -> id: parameter name
in_ -> openapi.IN_PATH: parameter location
description: parameter description
type: Schema or SchemaRef

function get

api/member/{id}/exists로 api 요청이 들어오면 get메서드가 실행된다. request 인자로 Request 객체의 인스턴스가 들어오고 id로는 해당 api 요청시 {id} 값이 들어오게 된다.

path("member/<str:id>/exists", ...

해당 id의 값은 URL.conf에서 그 변수를 설정해 줄 수 있다. <str:id> 에서 id가 그 부분이다.

...
def get(self, request, id):
    try:
        member = Member.objects.get(pk=id)
...
	#print(Member.objects) -> member.Member.objects
    #print(type(Member.objects)) -> member.managers.MemberManager

Member.objects의 경우 Member class 안에 MemberManager의 인스턴스를 objects 변수로 받고 있다. MemberManager의 경우BaseUserManager 클래스를 상속받고 있다. 그래서 print로 objects의 값과 타입을 찍으면 위와 같다.

Member 모델의 객체 중에서 기본키의 값이 id가 가진 값과 일치하는 객체를 조회하는 코드이다. get의 경우 하나의 객체를 반환시킨다. 객체가 존재하지 않을 경우 예외 발생시킨다.

class ExistsIdView(APIView):
	"""
    아이디 중복 체크
    """
    ...
    def get(self, request, id):
        try:
            member = Member.objects.get(pk=id)
        except Member.DoesNotExist:
            raise exceptions.NotFound("member not found")
request -> <rest_framework.request.Request: GET '/api/member/anonymous/exists'>

id -> anonymous

Serializer를 통한 유효성 검사

참고링크

# rest_framework/serializers.py
class BaseSerializer(Field):
    def __init__(self, instance=None, data=empty, **kwargs):

# instance -> 직렬화 목적
# data -> 유효성 검사

class Serializer(BaseSerializer):
    pass

data 인자가 있을 때

.is_valid() 호출 되서야

  • .initial_data에 접근 가능
  • .validated_data 통해 유효성 검증에 통과한 값들이 .save()시에 사용된다.
  • .errors -> 유효성 검증 수행 후에 오류 내역
  • .data -> 유효성 검증 후 갱신된 인스턴스에 대한 필드 값을 사전 형태로 전달

serialzer.save(**kwargs) 호출

  1. DB에 저장한 instance를 리턴
  2. .validated_data와 kwargs 사전을 합친 데이터

    .update 함수 .create 함수를 통해 관련 필드에 값을 할당하고 DB에 저장
    - .update(): self.instance 인자 지정했을 때
    - .create(): self.instance 인자 지정하지 않았을 때

is_valid 함수 호출 전까지의 과정

참고링크1
참고링크2
BooleanResponseSerializer(serializers.Serializer) -> Serializer(BaseSerializer) -> BaseSerializer(Field) 으로 상속을 받는다.

BaseSerializer의 생성자 함수에서 self.initial_data = data로 data의 값이 들어간다. 상위 부모 클래스에서 초기화 됐으므로 data는 이제 BaseSerializer 지점에서 정착해 있다.

is_valid 호출 후의 과정

이후 is_valid 함수를 호출하게 될 경우 인스턴스는 _validated_data 프로퍼티 메서드가 실행 된다.

1) 인스턴스 생성 시 data 인자에 값을 넘겨 주어야지 is_valid 함수를 호출할 수 있다.

2) res.is_valid()를 호출하지 않고 return 값으로 Response(res.data)를 return 하면 해당 api를 요청 시 아래와 같은 error를 맞는다.

  • When a serializer is passed a data keyword argument you must call .is_valid() before attempting to access the serialized .data representation.
    You should either call .is_valid() first, or access .initial_data instead

3) self.initial_data가 생성되면 is_valid 함수 내부 _validated_data의 속성을 생성하게 된다. self_validated_data의 값은 OrderedDict(['result', True])를 갖고 self.errors의 경우 {} 값을 갖는다. is_valid의 return 값은 not bool(self.errors)인데 빈 {}False를 반환하므로 결과적으로True를 반환하게 된다. 에러메세지가 있을 경우 False를 반환한다.

4) is_valid -> run_validation 함수는 2가지 로직을 갖는다. 첫번째는 data 매개인자로 들어온 값이 없을 때 이를 처리하여 값을 반환하는 경우 나머지는 안에 있는 값을 추출 후 유효성 검사를 하는 경우가 있다.

  • data 매개 인자로 아무 값이 들어오지 않았을 때 validate_empty_values 함수를 실행시킨다. 해당 함수 안에는 get_default 메서드가 있고 해당 함수는 data에 값이 지정되지 않았을 때 data에 기본 값을 부여하는 역할을 한다.
  • 값이 들어 온 경우 run_validators 함수로 유효성 검사를 진행한다.
  • 검사를 진행하기 전에 값을 추출한 후 통과하면 추출한 값을 반환한다.

is_valid

def is_valid(self, raise_exception=False):
    assert hasattr(self, 'initial_data'), (
        'Cannot call `.is_valid()` as no `data=` keyword argument was '
        'passed when instantiating the serializer instance.'
    )

    if not hasattr(self, '_validated_data'):
        try:
            self._validated_data = self.run_validation(self.initial_data)
        except ValidationError as exc:
            self._validated_data = {}
            self._errors = exc.detail
        else:
            self._errors = {}

    if self._errors and raise_exception:
        raise ValidationError(self.errors)

    return not bool(self._errors)

run_validation


def run_validation(self, data=empty):
    """
    Validate a simple representation and return the internal value.

    The provided data may be `empty` if no representation was included
    in the input.

    May raise `SkipField` if the field should not be included in the
    validated data.
    """
    (is_empty_value, data) = self.validate_empty_values(data)
    if is_empty_value:
        return data
    value = self.to_internal_value(data)
    self.run_validators(value)
    return value

validate_empty_values

def validate_empty_values(self, data):
    """
    Validate empty values, and either:

    * Raise `ValidationError`, indicating invalid data.
    * Raise `SkipField`, indicating that the field should be ignored.
    * Return (True, data), indicating an empty value that should be
      returned without any further validation being applied.
    * Return (False, data), indicating a non-empty value, that should
      have validation applied as normal.
    """
    if self.read_only:
        return (True, self.get_default())

    if data is empty:
        if getattr(self.root, 'partial', False):
            raise SkipField()
        if self.required:
            self.fail('required')
        return (True, self.get_default())

    if data is None:
        if not self.allow_null:
            self.fail('null')
        # Nullable `source='*'` fields should not be skipped when its named
        # field is given a null value. This is because `source='*'` means
        # the field is passed the entire object, which is not null.
        elif self.source == '*':
            return (False, None)
        return (True, None)

    return (False, data)

get_default 함수

def get_default(self):
    """
    Return the default value to use when validating data if no input
    is provided for this field.

    If a default has not been set for this field then this will simply
    raise `SkipField`, indicating that no value should be set in the
    validated data for this field.
    """
    if self.default is empty or getattr(self.root, 'partial', False):
        # No default, or this is a partial update.
        raise SkipField()
    if callable(self.default):
        if hasattr(self.default, 'set_context'):
            warnings.warn(
                "Method `set_context` on defaults is deprecated and will "
                "no longer be called starting with 3.13. Instead set "
                "`requires_context = True` on the class, and accept the "
                "context as an additional argument.",
                RemovedInDRF313Warning, stacklevel=2
            )
            self.default.set_context(self)

        if getattr(self.default, 'requires_context', False):
            return self.default(self)
        else:
            return self.default()

    return self.default

to_internal_value

def to_internal_value(self, data):
    """
    Transform the *incoming* primitive data into a native value.
    """
    raise NotImplementedError(
        '{cls}.to_internal_value() must be implemented for field '
        '{field_name}. If you do not need to support write operations '
        'you probably want to subclass `ReadOnlyField` instead.'.format(
            cls=self.__class__.__name__,
            field_name=self.field_name,
        )
    )

정리

스웨거

  • 스웨거 문서에 추가로 전달 할 정보를 manual_parameters 인자에 Parameter 인스턴스를 필요한 만큼 리스트 안에 넣는다.
  • 응답 받을 때 나타내지는 값을 responses 인자에 사전 형태로 응답 상태코드와 Serializer를 넣는다.

객체 조회

  • Member.objects.get(pk=id) 로 Model Member의 객체 하나를 조회할 수 있다. 객체가 없을 경우 에러를 뱉는다. pk=id 에서 id의 경우 URL.conf에서 지정할 수 있다.

유효성 검사

serialer.is_valid()를 통해 유효성 검사를 수행할 수 있다. 인스턴스를 매개변수로 넘길 경우 직렬화에 그 목적이 있고 data 인자로 넘길 경우 값에 대한 유효성을 검사하는 것에 그 목적이 있다.

결론

멤버 테이블의 기본키 목록에 인자로 받은 값이 있는지 없는지를 조회한 후 값이 없을 때 에러를 뱉고 값이 있을 때에는 T/F를 뱉는 시리얼라이저에 데이터를 넣어 유효성 검사를 수행한 이후 그 결과 값을 리턴한다. 그 값은 True이다.

class ExistsIdView(APIView):
    """
    아이디 중복 체크를 

    ---
    """

    permission_classes = (permissions.AllowAny,)

    id_param = openapi.Parameter(
        "id", openapi.IN_PATH, description="user id", type=openapi.TYPE_STRING
    )

    @swagger_auto_schema(
        manual_parameters=[id_param], responses={200: BooleanResponseSerializer}
    )
    def get(self, request, id):
        try:
            member = Member.objects.get(pk=id)
        except Member.DoesNotExist:
            raise exceptions.NotFound("member not found")

        res = BooleanResponseSerializer(data={"result": True})
        if res.is_valid():
            return Response(res.data)

0개의 댓글