Django Rest Framework(DRF) APIView 동작 원리 분석

Hoonkii·2022년 9월 5일
2

사내에서 Rest API 개발 프레임워크로 Django Rest Framework(DRF)를 사용 중이다. DRF에서 APIView는 Class Based View의 일종으로 클래스 안에 serializer나 permission, throttle 클래스를 명시해주면 알아서 명시된 클래스들로부터 객체를 생성해 주입하여 api에 해당 기능들을 부여해준다.

어떻게 동작하는지 궁금해서 코드를 통해서 원리를 분석해보았다.

APIView.as_view()

APIView를 url과 연결하기 위해서는 url_patterns 에 APIVIew.as_view()를 명시해야 한다.

path("me", UserDetailApi.as_view(), name="user_detail")

위 코드 처럼 as_view()함수를 호출하면서 지정한다. 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

as_view() 함수는 request-response process의 메인 엔트리포인트이다. 유효한 http 요청인지 검증 후 APIView안에 있는 dispatch() 함수를 통해 실제 요청을 위임한다.

APIView.dispatch()

APIView 클래스의 dispatch() 함수 코드를 살펴보자.

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) # 요청 정보 초기화 및 converting
    self.request = request
    self.headers = self.default_response_headers 

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

        # 적절한 핸들러 메소드 위임
        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

dispatch 함수는 django 프레임워크의 class based view의 dispatch와 비슷하지만, 예외 처리나 초기 permission check와 같은 훅을 제공한다.

실행 흐름을 큰 틀에서 정리하면 다음과 같다.

STEP 1. wsgi에서 온 요청을 drf에서 사용하는 요청으로 convert 한다. self.initialize_request
STEP 2. 요청을 핸들러에게 위임하기 전 permission, throttling 등의 처리를 수행한다. self.initialize
STEP 3. request method를 확인하고 APIView에 request method에 해당하는 이름의 handler가 있는지 검증한다.
STEP 4. 실제 요청을 위임한다. response = handler(request, *args, **kwargs)
STEP 4-a. 요청 중 발생한 에러가 있다면 잡아서 핸들링 한다. self.handler_exception(exc)
STEP 5. 최종 response를 변환하는 작업을 거친 후 리턴한다. self.finalize_response(request, response, *args, **kwargs)

STEP 1. initialize_request()

def initialize_request(self, request, *args, **kwargs):
    """
    Returns the initial request object.
    """
    parser_context = self.get_parser_context(request)

    return Request(
        request,
        parsers=self.get_parsers(),
        authenticators=self.get_authenticators(),
        negotiator=self.get_content_negotiator(),
        parser_context=parser_context
    )

drf에 default로 설정된 parser, authenticator 등을 활용해 parser, authenticator들을 설정한다. 만약 APIView 클래스 안에 설정된 parser나 authenticator 클래스가 있다면 해당 클래스로부터 객체를 생성해 주입시킨다.

default 설정은 settings.py에 다음과 같이 정의한다.

REST_FRAMEWORK = {
    "DEFAULT_PARSER_CLASSES": (
        "djangorestframework_camel_case.parser.CamelCaseJSONParser",
        "djangorestframework_camel_case.parser.CamelCaseMultiPartParser",
        "djangorestframework_camel_case.parser.CamelCaseFormParser",
    ),
    "DEFAULT_AUTHENTICATION_CLASSES": [
        # Use simple jwt authentication
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}

Step 2. initial()

def initial(self, request, *args, **kwargs):
    """
    Runs anything that needs to occur prior to calling the method handler.
    """
    self.format_kwarg = self.get_format_suffix(**kwargs)

    # Perform content negotiation and store the accepted info on the request
    neg = self.perform_content_negotiation(request)
    request.accepted_renderer, request.accepted_media_type = neg

    # Determine the API version, if versioning is in use.
    version, scheme = self.determine_version(request, *args, **kwargs)
    request.version, request.versioning_scheme = version, scheme

    # Ensure that the incoming request is permitted
    self.perform_authentication(request)
    self.check_permissions(request)
    self.check_throttles(request)

request와 관련된 값을 converting 한 후 실제 요청을 처리하기 전에 필요한 값 혹은 검사들을 initialize를 통해 수행한다.

하나의 예씨로 permissions 쪽 코드를 살펴보자.

def get_permissions(self):
    """
    Instantiates and returns the list of permissions that this view requires.
    """
    return [permission() for permission in self.permission_classes]
def check_permissions(self, request):
    """
    Check if the request should be permitted.
    Raises an appropriate exception if the request is not permitted.
    """
    for permission in self.get_permissions():
        if not permission.has_permission(request, self):
            self.permission_denied(
                request,
                message=getattr(permission, 'message', None),
                code=getattr(permission, 'code', None)
            )

APIView에서 정의된 permission class를 기반으로 객체를 생성해서 주입해주고, permission 객체의 has_permission함수를 호출해 권한을 검증한다.

class SignupWithTokenApi(GenericAPIView):

    permission_classes = [AllowAny]

위의 예시는 클래스 지정 예시인데, permission_classes에 지정될 클래스들은 BasePermission 인터페이스를 구현해야 한다.

STEP 3, 4 는 요청을 위임하는 것이기 때문에 internal call은 따로 없다.

Step 4-a. self.handle_exception()

def handle_exception(self, exc):
    """
    Handle any exception that occurs, by returning an appropriate response,
    or re-raising the error.
    """
	# 만약 인증 실패에 대한 exception이라면 auth header에 따라 적절한 처리를 수행한다. 
    if isinstance(exc, (exceptions.NotAuthenticated,
                        exceptions.AuthenticationFailed)):
        auth_header = self.get_authenticate_header(self.request)

        if auth_header:
            exc.auth_header = auth_header
        else:
            exc.status_code = status.HTTP_403_FORBIDDEN

    exception_handler = self.get_exception_handler() # settings.py에 정의된 common exception handler 클래스를 객체로 생성해서 리턴. 

    context = self.get_exception_handler_context()
    response = exception_handler(exc, context)

    if response is None:
        self.raise_uncaught_exception(exc)

    response.exception = True
    return response

exception_handler = self.get_exception_handler() 에서 self.get_exception_handler()

return self.settings.EXCEPTION_HANDLER

DRF의 settings에서 설정한 exception handler를 가져온다. exception handler에서 별도의 exception converting이 있다면 처리되고, exception response를 리턴하게 된다.

REST_FRAMEWORK = {
	"EXCEPTION_HANDLER": "common.exceptions.custom_exception_handler"
}

위 처럼 settings에 명시된 DRF Exception Handler 클래스를 통해 객체를 생성한다.

Step 5. self.finalize_response()

def finalize_response(self, request, response, *args, **kwargs):
    """
    Returns the final response object.
    """
    # Make the error obvious if a proper response is not returned
    assert isinstance(response, HttpResponseBase), (
        'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` '
        'to be returned from the view, but received a `%s`'
        % type(response)
    )

    if isinstance(response, Response):
        if not getattr(request, 'accepted_renderer', None):
            neg = self.perform_content_negotiation(request, force=True)
            request.accepted_renderer, request.accepted_media_type = neg

        response.accepted_renderer = request.accepted_renderer
        response.accepted_media_type = request.accepted_media_type
        response.renderer_context = self.get_renderer_context()

    # Add new vary headers to the response instead of overwriting.
    vary_headers = self.headers.pop('Vary', None)
    if vary_headers is not None:
        patch_vary_headers(response, cc_delim_re.split(vary_headers))

    for key, value in self.headers.items():
        response[key] = value

    return response

response와 관련되어 accept_renderer, accepted_media_type 등 필요한 정보들을 세팅하고 response를 확정하여 리턴한다.

처리 과정을 요약하자면, APIView의 as_view() 함수를 통해서 유효한 요청인지 먼저 검증 후 dispatch() 함수를 통해 실제 요청을 위임한다. dispatch() 과정에서 settings.py에 기본 명시된 클래스들 혹은 APIView 클래스 정의 시 명시된 클래스들 (e.g serializer_class, permission_class 등) 을 기반으로 객체를 생성해 주입하고 관련 기능들을 실행시킨다. 만약 에러가 발생하면 settings.py에 정의된 custom error handler를 통해 에러를 처리할 수 있도록 하고, 에러가 발생하지 않으면 설정에 따라 정제된 응답을 리턴한다.

정리

DRF APIView의 실행 흐름을 분석해보았다. 코드가 어렵지 않고 직관적으로 작성되어 있어서 이해하기 수월하였다. authentication, permission, throttling 등 애플리케이션 로직과 분리되어 공통으로 처리될 수 있는 기능들을 인터페이스로 정의하고, 사용 시 인터페이스를 구현한 클래스를 명시하면 해당 클래스로부터 객체를 생성 후 주입하여 기능들을 실행시키는 점이 인상 깊었다. 소프트웨어 공학에서 말하는 DI, AOP 매커니즘이 적용된 것이다.

코드 분석을 통해서 추상화 및 재사용 설계 패턴을 엿볼 수 있었고, 내부 구현 메커니즘을 파악하고 나서 APIView 커스텀이 한결 용이해졌다.

profile
개발 공부 내용 정리

0개의 댓글