본문 바로가기

Frame Work/Django

[Django] show_urls 흉내내기

728x90
반응형



개요 

django_extensions은 django 개발시 사용할 수 있는 편리한 추가 기능을 제공하는 Third Party 라이브러리입니다. 이 라이브러리는 django 프로젝트의 개발 및 디버깅을 보다 쉽게 만들어줍니다.

여러 가지 기능들이 있겠지만 그중에서도 저는 django에 정의된 view를 확인하는 데 많이 사용하고 있으며 문서를 작성할 때 프로젝트에 정의된 총 view의 수를 측정할 때 사용하곤 합니다. (view의 총개수를 본다는 건 얼마나 많은 API가 정의되어 있는지를 의미합니다)

그런데 정의된 view만 확인하는 것 외에 이것을 이용하여 추가로 필요한 정보들을 측정하려면 제공되는 기능만 이용해서는 부족하다고 했습니다. 적절히 활용하기 위해 django-extensions는 어떻게 view의 개수를 세는지 탐구해 보고 이를 코드로 구현해 보기로 했습니다.

직접적인 설명에 앞서 이 글에서는 django 프로젝트의 구조는 다음과 같습니다.

.
├── app
│   ├── __init__.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── bin
│   └── show_urls.py
├── db.sqlite3
├── manage.py
└── src
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

# app/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("first/view", views.example_view, name="first-view"),
    path("second/view", views.example_view, name="second-view")
]

 

django-extensions에서 view를 확인하는 방법

먼저 django-extensions에서 정의된 view를 확인하는 방법은 show_urls 명령을 사용하는 것입니다.

$ python manage.py show_urls

결과는 다음과 같습니다.

/api/first/view	app.views.example_view	first-view
/api/second/view	app.views.example_view	second-view

show_urls에 -f table 옵션을 주게 되면 shell에서 table 형태로 표출되기도 합니다.

URL                 | Module                          | Name         | Decorator
----------------------------+-----------------------------------------+----------------------+-
/api/first/view  | app.views.example_view | first-view  |
/api/second/view | app.views.example_view | second-view |

django extensions에서 기본적으로 제공되는 명령어만으로도 django project에 존재하는 view를 확인하는 것 자체는 문제가 없습니다.

하지만 정의된 view의 수가 한 번에 세기 복잡할 정도로 많아진다면 난해한 상황이 생깁니다.

예를 들어 view_name이 지정된 것과 지정되지 않은 것의 수를 구해야 하는 상황이라던가 어떤 app에 몇 개의 view가 존재하는지를 파악해야 한다면 기본적으로 제공되는 명령어만으로는 부족합니다.


django-extensions은  view를 어떻게 표출하는 걸까?

이제 django-extensions가 어떻게 view를 shell에 표출해 주는지 알아봅시다. django_extensions가 설치된 위치를 찾아 들어갑니다. 저는 venv를 생성하고 django extenions을 설치했으니 venv 디렉터리로 이동해서 찾을 수 있었습니다.

/venv/lib/python3.10/site-packages/django_extensions/management/commands/show_urls.py

show_urls.py을 읽다 보면 눈에 띄는 메서드가 존재합니다.

urlpatterns로부터 view list를 추출한다고 되어있습니다. view를 표출하기 위해 구현된 핵심 메서드네요. 이후 코드를 쭉 읽다 보면 loop를 하는 과정에서 URLPattern과 RegexURLPattern이라는 것을 사용하고 있는 게 보입니다. 이 중 URLPattern과 RegexURLPattern 객체가 눈에 띄는데 코드 상단부로 이동해서 살펴보면 다음과 같이 선언되어 있습니다.

from django.urls import URLPattern, URLResolver  # type: ignore


class RegexURLPattern:  # type: ignore
    pass

RegexURLPattern은 이상해 보여도 URLPattern은 django.urls에서 import 하고 있는 것으로 보아 django에서 기본적으로 제공되는 객체인듯합니다. 이제 그 옆의 URLResolver와 함께 무엇을 하는지 구글링 하자면 이 두 객체에 대해 다음과 같이 소개하고 있습니다.

URLPattern 클래스는 URL 패턴을 나타내는 객체입니다. 예를 들어, '/about/' URL 패턴에 대한 URLPattern 객체는 다음과 같이 정의될 수 있습니다.

from django.urls import path
from myapp.views import AboutView

urlpatterns = [
    path('about/', AboutView.as_view(), name='about'),
]

URLResolver 클래스는 URL 패턴에 대한 라우팅을 처리하는 객체입니다. 예를 들어, 다음과 같은 URL 패턴을 가진 앱이 있다고 가정해 봅시다.

from django.urls import path
from .views import HomeView, ContactView

urlpatterns = [
    path('', HomeView.as_view(), name='home'),
    path('contact/', ContactView.as_view(), name='contact'),
]

위 코드에서 URLResolver 객체는 urlpatterns 변수에 할당된 리스트 내의 path() 함수 호출 결과입니다. URLResolver 객체는 해당 URL 패턴에 대한 요청이 발생할 때 실행될 뷰 함수 또는 클래스를 결정하는 역할을 합니다.

즉 django extensions에서는 django project에 정의된 view의 목록을 표출하기 위해서 URLPattern과 URLResolver 객체를 이용한다는 것을 알 수 있습니다.

URLPattern과 URLResolver로 직접 구현해보기

직접 구현하기 위해 이제 어떤 정보들이 필요한지 파악되었으니 손수 구현해 봅시다. 일단 최상위 경로인 ‘/’로부터 시작하는 목록을 찾기 위해 django settings로부터 root 경로를 미리 가져옵시다

ROOT_URL_CONF = settings.ROOT_URLCONF

이후 URLResolver를 이용해 ROOT URL로부터 시작하는 경로들을 가져옵시다.

@cached_property
def root(self) -> URLResolver:
    return URLResolver(RegexPattern(r"^/"), self.ROOT_URL_CONF)

URLResolver 객체는 url_patterns라는 property를 가지고 있습니다. 이 property를 이용해 sub url pattern을 차례로 loop 하면 정의된 view의 정보를 가져올 수 있습니다.

def collect(self) -> List[EndPointMeta]:
    root_patterns: List[URLResolver] = [root_url_pattern for root_url_pattern in self.root.url_patterns]

    objs = []
    for root_pattern in root_patterns:
        for sub_pattern in root_pattern.url_patterns:
            objs.append(EndPointMeta(
                prefix=str(root_pattern.pattern),
                pattern=str(sub_pattern.pattern),
                name=str(sub_pattern.name))
            )

    return objs

sub_pattern은 URLPattern이라는 객체인데 이 객체를 통해 해당 view에 접근이 가능하게 됩니다. 추가로 EndPointMeta는 각 view 정보를 다시 처리하기 위해 선언한 class이며 다음과 같은 형태입니다.

class EndPointMeta:
    def __init__(self, prefix, pattern, name):
        self.prefix = prefix
        self.pattern = pattern
        self.name = name

실행하면 다음과 같은 결과가 나옵니다.

prefix                    | pattern                   | Name                     
api/                      | first/view                | first-view               
api/                      | second/view               | second-view

 

Class View도 가져올 수 있나?

그렇다면 view가 class로 정의되어있는 경우, 이 class 정보를 가져올 수 있을까도 확인해봅시다. 우선 다음과 같이 간단한 class view를 선언합시다.

from django.views import View
from django.http.response import 

class ExampleView(View):
    @decorator
    def get(self):
        return HttpResponse()

이제 Class View에 접근이 가능한지 sub_pattern의 namespace를 확인해봅시다.

from django.urls.resolvers import URLPattern

for root_pattern in root_patterns:
    for sub_pattern in root_pattern.url_patterns:
        sub_pattern: URLPattern
        print(dir(sub_pattern))

결과는 다음과 같습니다.

['__class__', ..., '_check_callback', '_check_pattern_name', 'callback', 'check', 'default_args', 'lookup_str', 'name', 'pattern', 'resolve']

여기서 callback이라는 property가 보이는데, 다시 이 callback의 property의 namespace를 확인해보는 다음과같이 view_class가 들어있습니다.

['__annotations__', ..., 'view_class', 'view_initkwargs']

이 view_class를 통해 Class View를 가져올 수 있습니다.

for root_pattern in root_patterns:
    for sub_pattern in root_pattern.url_patterns:
        sub_pattern: URLPattern
        print(sub_pattern.callback.view_class)
    
# OutPut
<class 'app.views.ExampleView'>

URLPattern을 통해 개발자가 정의한 Class View에 접근할 수 있다는 건 의미가 크다고 보입니다. 어떤 http method가 정의되었는지를 확인할 수도 있으며 inspect 라이브러리 조합해 다양한 도구를 만들어낼 수 있습니다. 

 

어떻게 써먹을 수 있을까?

직접 구현하는 것까지 마치셨다면 사실 여기서부터는 이를 어떻게 다룰 것인지는 상황에 따라 다르다 생각됩니다. 제가 써먹었던 방식을 적어보자면 다음과 같이 Report라는 class를 만들고 측정에 필요한 로직을 구현해 나가는 방식을 이용했습니다.

class Report:
    def __init__(self, endpoint_meta: List[EndPointMeta]):
        self.endpoint_meta = endpoint_meta

    def completed(self):
        """ 작업이 완료된 view """ 
        count = 0
        for endpoint_meta in self.endpoint_meta:
            if endpoint_meta.name != 'None':
                count += 1
        return count

    def all_endpoint(self):
        """ 모든 view의 수 """ 
        return len(self.endpoint_meta)

만약 django project와 관련되어 정량적 지표가 필요한 상황에서 view에 관련된 무언가를 확인해야한다면 본 포스팅에서 기재한 방법을 고려해보는 게 어떨까요?

 

Reference

전체 코드는 제 gist 링크를 남겨놓습니다.

728x90
반응형

'Frame Work > Django' 카테고리의 다른 글

Django와 MySQL 연결 문제 Exception 탐구  (0) 2023.06.06
Django project를 src layout으로 구성하기  (0) 2023.01.30
[Django] auth_group 다루기  (0) 2022.12.18
uWSGI Socket + Nginx + Docker  (0) 2022.11.10
uWSGI를 알아보자  (0) 2022.11.05