IT/Django

django @login_required 데코레이터 작동 원리 이해

bepuri 2024. 12. 5. 15:59
728x90

데코레이터는 특정 함수가 작동할 때에 함수 로직의 코드 수정 없이, 추가적인 로직을 주입하기 위해 사용한다.

데코레이터에 대한 이해는 아래 링크를 통해서 해보시고,

파이썬 코딩 도장: 42.1 데코레이터 만들기

django에서 내장된 login_required 데코레이터만 살펴 보겠다.

def login_required(
    function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None
):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = user_passes_test(
        lambda u: u.is_authenticated,
        login_url=login_url,
        redirect_field_name=redirect_field_name,
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

login_required 함수에는 wrapper함수도 없고, actual_decorator를 반환해줄 뿐이기에,
actual_decorator 정의부에 대한 것은 user_passes_test 함수를 정확히 살펴봐야 하는 것이다.

따라서 핵심 함수는 user_passes_test 함수이므로, 함수 호출 시에 넘어가는 인자에 대해 하나씩 살펴보겠다.

첫번째 인자 lambda u: u.is_authenticated
lambda 또한 설명 범위를 넘어가니, 간단히 해당 함수의 역할에 대해 설명만 하겠다.
user가 인증되어있는지를 여부를 리턴해주는 익명함수이다.

실제로 아래 user_passes_test함수의 test_func 인자로 lambda 함수가 넘어가며, 해당 함수를 실행하고 test_pass 여부에 따라 페이지를 보여줄지, login 페이지로 redirect할지 결정하는 코드가 L35 , L47에 위치하고 있다.

코드가 두가지로 나뉘는 이유는 비동기여부에 따라 분기 처리 되어있기 때문이다.

두번째 인자 login_url은 L13 에서 resolve_url함수를 통해서 resolved_login_url을 가져오기 위해 전달하는 것이다.

마지막 인자 redirect_field_name의 경우 로그인 페이지 이동 시에 로그인 성공 후 어느 페이지로 이동할 지 querystring에 선언하는 것이 일반적인데 해당 필드 이름을 설정하는 인자이다.
만약 설정하지 않으면 아래 L2에서 확인할 수 있듯이, REDIRECT_FIELD_NAME 상수 값인 next가 들어간다

def user_passes_test(
    test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME
):
    """
    Decorator for views that checks that the user passes the given test,
    redirecting to the log-in page if necessary. The test should be a callable
    that takes the user object and returns True if the user passes.
    """

    def decorator(view_func):
        def _redirect_to_login(request):
            path = request.build_absolute_uri()
            resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
            # If the login url is the same scheme and net location then just
            # use the path as the "next" url.
            login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
            current_scheme, current_netloc = urlparse(path)[:2]
            if (not login_scheme or login_scheme == current_scheme) and (
                not login_netloc or login_netloc == current_netloc
            ):
                path = request.get_full_path()
            from django.contrib.auth.views import redirect_to_login

            return redirect_to_login(path, resolved_login_url, redirect_field_name)

        if asyncio.iscoroutinefunction(view_func):

            async def _view_wrapper(request, *args, **kwargs):
                auser = await request.auser()
                if asyncio.iscoroutinefunction(test_func):
                    test_pass = await test_func(auser)
                else:
                    test_pass = await sync_to_async(test_func)(auser)

                if test_pass:
                    return await view_func(request, *args, **kwargs)
                return _redirect_to_login(request)

        else:

            def _view_wrapper(request, *args, **kwargs):
                if asyncio.iscoroutinefunction(test_func):
                    test_pass = async_to_sync(test_func)(request.user)
                else:
                    test_pass = test_func(request.user)

                if test_pass:
                    return view_func(request, *args, **kwargs)
                return _redirect_to_login(request)

        # Attributes used by LoginRequiredMiddleware.
        _view_wrapper.login_url = login_url
        _view_wrapper.redirect_field_name = redirect_field_name

        return wraps(view_func)(_view_wrapper)

    return decorator

데코레이터를 아무런 생각없이 사용을 꽤 오랬동안 사용해왔었는데,
이렇게 디테일하게 코드를 이해하는 시간이 없으면 아무런 생각없이 코드를 작성하게 되기 쉬운 것 같다.

좋은 코드를 분석 함으로서, 더 나은 개발자로 거듭날 수 있다고 생각한다.

728x90