ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] DRF Router(라우터) 동작 원리
    파이썬/Django 2022. 2. 13. 22:42

    들어가며

    DRF의 대표 클래스(ViewSet, Serializer, Router)중 하나인 Router의 종류와 동작원리를 알아본다. Router는 간단하고 빠르게 자동으로 url 라우팅을 할 수 있도록 해준다.

    from rest_framework import routers
    from apps.account. import views
    
    router = routers.SimpleRouter()
    router.register(r'users', views.UserViewSet)
    urlpatterns = router.urls

    위와 같은 코드를 작성하면

    • ^users/$
    • ^users/{int: lookup}/$

    의 url패턴이 자동으로 생성된다.

    DRF에는 SimpleRouter, DefaultRouter 두 가지를 제공한다.

    SimpleRouter

    이 라우터를 사용하면 기본적으로 ViewSet에 있는 list, create, retrieve, update, partial_update, destory 액션들에 대해서 라우팅을 해준다.

    ❗ 액션이란 Viewset안에서 router를 통해 request를 처리할 수 있는 메서드이다. `엔드포인트`라고 알면 되겠다. 나중에 ViewSet의 classmethod를 동해 view로 바뀐다. (1 action = 1 view)

    DRF 공식문서에서 제공하는 표이다.

    URL Style HTTP Method Action URL Name
    {prefix}/ GET list {basename}-list
    POST create    
    {prefix}/{url_path}/ GET, or as specified by methods argument @action(detail=False) decorated method {basename}-{url_name}
    {prefix}/{lookup}/ GET retrieve {basename}-detail
    PUT update    
    PATCH partial_update    
    DELETE destroy    
    {prefix}/{lookup}/{url_path}/ GET, or as specified by methods argument @action(detail=True) decorated method {basename}-

    이렇게 Router에 정의된 basename, prefix에 따라 list, create, retrieve, update, partial_update, destory 액션에 대해 라우팅이 자동으로 되며 @action 데코레이터의 정의에 따라 라우팅이 이루어진다.

    DefaultRouter

    위의 SimpleRouter와 비슷하지만 추가로 자동으로 root endpoint를 생성한다.

    root endpoint는 모든 뷰셋의 list action endpoint를 하이퍼링크 또는 json 형식으로 반환해준다.

    # views.py
    class UserAPIViewSet(GenericViewSet):
        ...
        def list(self, request):
            data = self.get_serializer(self.get_queryset(), many=True).data
            return Response(status=200, data=data)
    
    class ProfileAPIViewSet(GenericViewSet):
        ...
        def retrieve(self, request);
            data = self.get_serialzer(self.get_object()).data
            return Response(status=200, data=data)
    
    # urls.py
    app_name = 'api'
    
    router_v1 = routers.DefaultRouter(trailing_slash=False)
    router_v1.register('user', UserAPIViewSet, basename='user')
    router_v1.register('profile', ProfileAPIViewSet, basename='profile')
    
    urlpatterns = [
        path('v1/', include(router_v1.urls))
    ]

    위와 같이 정의되어 있을 경우

    https://www.domain.com/api/v1/ 에 요청할 경우 response는

    {
        'user': "https://www.domain.com/api/v1/user"
    }

    이 된다. ProfileAPIViewSet 에는 list action이 없으므로 포함되지 않는다.

    Router 동작 원리

    router가 사용되는 코드를 보면서 동작원리를 알아보면

    # 1.
    router_v1 = routers.SimpleRouter(trailing_slash=False)
    
    # 2.
    router_v1.register('user', UserAPIViewSet, basename='user')
    router_v1.register('profile', ProfileAPIViewSet, basename='profile')
    
    # 3.
    urlpatterns = [
        path('v1/', include(router_v1.urls))
    ]

    1. SimpleRouter 객체를 생성한다.

    2. register 메서드를 사용해서 viewset을 등록한다. registry 라는 리스트 애트리뷰트에 (prefix, viewset, basename) 순으로 tuple형태로 추가한다.

    3. urls 프로퍼티 접근 시 registry를 사용해서 URL을 자동으로 생성한다.

     

    이 부분을 더 자세하게 로직을 들여다 보면

     

    3-1. 2번에서 등록한 registry를 iterate하면서 viewset마다 route들을 가져온다.

    아래는 route들을 가져오는 로직이다.

    def get_routes(self, viewset):
        # 1) 
        known_actions = list(flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)]))
        # 2)
        extra_actions = viewset.get_extra_actions()
    
        # 3)
        not_allowed = [
            action.__name__ for action in extra_actions
            if action.__name__ in known_actions
        ]
        if not_allowed:
            msg = ('Cannot use the @action decorator on the following '
                   'methods, as they are existing routes: %s')
            raise ImproperlyConfigured(msg % ', '.join(not_allowed))
    
        # 4)
        detail_actions = [action for action in extra_actions if action.detail]
        list_actions = [action for action in extra_actions if not action.detail]
    
        routes = []
        # 5)
        for route in self.routes:
            if isinstance(route, DynamicRoute) and route.detail:
                routes += [self._get_dynamic_route(route, action) for action in detail_actions]
            elif isinstance(route, DynamicRoute) and not route.detail:
                routes += [self._get_dynamic_route(route, action) for action in list_actions]
            else:
                routes.append(route)
    
            return routes
    """
    1) 기본으로 라우터에 정의되어 있는list, create, retrieve, update, partial_update, destory 액션들을 가져온다. (3번에서 체크를 하기위해)
    
    2) viewset에 @action 데코레이터가 적용된 메서드들을 가져온다. → extra_actions
    
    3) extra_actions들 중에 기본으로 라우터가 제공하는 동일한 이름의 action이 있는지 체크한다.
    
    4) extra_actions 를 detail에 따라 나눈다.(detail=True면 prefix뒤에 lookup(보통 pk)이 붙는다.)
    
    5) 기본으로 라우터에 정의되어 있는 경로들 + detail=True인 경로 + detail=False인 경로들을 모두 리스트에 넣어 반환한다.
    """

    3-2 가져온 route들을 viewset의 클래스 메서드인 as_view를 사용해서 각각의 view를 만들어준다.

    3-3 마지막으로, 만든 view를 사용해 regex path를 만들어 결과 리스트에 append해준다.

     

    마치며

    DRF의 라우터의 종류 및 동작원리를 알아보았다. 각 action은 view로 바뀌게 된다. SimpleRouter, DefaultRouter의 차이점은 크게 다르지 않다. 단지, root endpoint를 생성해주냐 안해주냐의 차이다.

    댓글

Designed by Tistory.