Python

Python Class Decorator 인자 값 전달 (Django, DRF, 권한 검증)

뒷골목프로그래머 2022. 11. 22. 19:15
반응형

안녕하세요.

글쓰는 개발자 입니다.

 

 Python Decorator 에 관해 찾아보던 중 대부분의 자료가 가장 기본적인 case (인자를 전달하지 않는 함수형 Decorator) 만을 다루기에 다음과 같이 제가 필요로 하는 기능을 만족하는 Decorator 를 소개하고자 합니다.

1. Decorator 가 인자를 받아 올 수 있어야 함
2. Decorator 에 의해 wrapping 되는 method 의 인자로 받아 올 수 있어야 함

 Decorator에 대한 Code Sample 만 소개하는 것 보다는 실무에서 제가 '어떤 상황에서 어떤 문제를 해결하기 위해 사용 했는지', 문제를 정의 하고 설계 및 구현에 이르기 까지의 과정을 간단히 소개하며 작성 했습니다. 구현 속에 Decorator 기능을 포함 시켰습니다. Decorator의 기능 구현 및 Code Sample 만 필요하신 분들은 최하단의 '부록'을 참고하시기 바랍니다.

 

이 글을 읽고 여러분은 다음과 같은 내용을 습득하실 수 있습니다.

1. Django Project 에서 API 별 Auth, Role 검증 구현
2. Python Decorator (class형)
3. ‘__init__’ 과 ‘__call__’ 의 차이

 

목차

1. 문제 정의

     1) 목표

     2) 설계

2. 구현

     1) Role, Auth Level 수치화

     2) Role, Auth 검증 로직 개발

     3) 검증 로직 적용

3. 부록

     1) Python Decorator (class)

     2) __init__ and __call__ method

4. 참고

 

 

1. 문제 정의

     1) 목표 : 서로 상이한 Role, Auth 검증을 모두 수행 할 수 있는 기능 개발

     2) 상황 : Auth, Role 에 대한 정책이 복잡하기 때문에 설계가 잘못 될 경우, 기능이 추가에 따른 검증 로직 개발 비용 증가

           * Auth : User의 고유한 권한, Level에 따라 분류 되며 상위 레벨은 하위 레벨의 모든 권한을 가짐

           * Role : Auth Level 에 따라 가질 수 있는 역할, 한명의 User가 Auth Level에 따라 여러 개의 Role을 부여받을 수도 있음

     3) 설계

          (1) 권한 검증 로직 수행 위치 : View (Django MVT 패턴 중 V) 에서 Decorator 를 활용

                * View 에 Decorator만 간편하게 적용 가능

                * Django Custom Middleware 에 적용 할 경우, 검증 로직이 필요 없는 API 가 불필요하게 검증 로직을 통과

Django Middleware (출처 : https://docs.djangoproject.com/en/1.8/topics/http/middleware/)

                   

          (2) 필요 기능 정의

                * 로그인 하여 Request 를 보내는 User의 Auth, Role 불러오기

                * 검증 Decorator 에 API 별로 제한하는 Auth, Role 인자로 전달

                * User의 Auth, Role 과 API 별로 제한하여 인자로 전달한 Auth, Role 비교하여 검증

 

2. 구현

     1) Role, Auth Level 수치화

           정책이 복잡하지만 다행인 것은 Auth, Role 간에 상하관계가 있으며, 상위 Level은 하위 Level 의 모든 권한을 포함하는 것입니다. 여기서 힌트를 얻어서 문자로 표현된 Auth 와 Role 을 수치화 하여 크기로 비교 할 수 있도록 Enum class 로 정의 하였습니다. 

# /api/enum.py
from enum import Enum


class NumericAuthLevel(int, Enum):
    auth_lv1: int = 10
    auth_lv2: int = 20
    auth_lv3: int = 30
    auth_lv4: int = 40


class NumericRoleLevel(int, Enum):
    role_lv1: int = 1
    role_lv2: int = 2
    role_lv3: int = 3

     2) Role, Auth 검증 로직 개발

          * API에서 제한하는 Auth, Role 을 수치화 하여 초기화 (’__init__)

          * Request를 보내는 User의 Auth, Role 수치화 (’__call__)

          * User의 Auth, Role과 API에서 제한하는 Auth, Role 비교 (’__call__)

# /api/decorator.py
from django.core.exceptions import BadRequest

from api.enum import NumericAuthLevel
from api.enum import NumericRoleLevel


class Decorator:
    def __init__(self, auth_level: int, role_level: int):
        self.auth_level: int = auth_level
        self.role_level: int = role_level

    def __call__(self, function):
        def wrapper(*args, **kwargs):
            request_auth: str = args[1].user.auth # 아래 설명 참고
            request_role: str = args[1].headers['role'] # 아래 설명 참고

            user_auth_level: int = NumericAuthLevel[request_auth].value
            user_role_level: int = NumericRoleLevel[request_role].value

            is_auth_valid: bool = user_auth_level >= self.auth_level
            is_role_valid: bool = user_role_level >= self.role_level

            if is_auth_valid and is_role_valid:
                return function(*args, **kwargs)
            else:
                raise BadRequest('Invalid User')

        return wrapper
  • args[1] : View 에서 전달하는 인자들 중에서 request를 받아오기 위함
  • args[1] .user.auth : request user의 auth (본 프로젝트에만 해당)
  • args[1].headers['role'] : request header의 ‘role’ (본 프로젝트에만 해당)

     

 

     3) 검증 로직 적용

          * 검증 로직이 필요한 API method 에 Decorator 를 적용

          * API 가 제한하는 권한을 Parameter 로 전달

# /api/views.py
from django.http import HttpRequest
from rest_framework import views
from rest_framework.response import Response

from api.decorator import Decorator
from api.enum import NumericAuthLevel
from api.enum import NumericRoleLevel


class SomeApi(views.APIView):
    @Decorator(NumericAuthLevel.auth_lv3.value, NumericRoleLevel.role_lv3.value)
    def get(self, request: HttpRequest):
        result: str = Something().get()
        return Response(result)

 

3. 부록

     1) Python Decorator (class)

단순한 예제 코드만 제공합니다. Decorator, Closure, 중첩 등에 관한 내용은 추후 자세히 다루겠습니다.

 

          Decorator는 개발자들이 method나 class 의 동작을 수정 할 수 있게끔 하기 때문에 매우 강력하고 유용한 도구 입니다. Decorator는 다른 function 을 wrapping 하는 방식으로 사용 됩니다.

Decorator가 강력한 이유는 wrapping 된 function 을 수정하지 않은 채, 기능 확장, 추가 등이 가능하기 때문입니다.

보통 Python Decorator 를 검색하면, 인자 값을 넘기지 않는 함수형 Decorator 들이 많이 나옵니다. 저는 인자 값을 전달 할 수 있는 Class 형 Decorator를 소개하겠습니다.

class 형 Decorator는 call method 를 활용합니다. user가 함수 처럼 동작하는 객체를 생성 할 때 Decorator는 함수처럼 동작 하는 객체를 전달 받아야 합니다. 이 때 ‘call’ method가 유용합니다. (’call’ 은 아래에서 다시 살펴봅니다.)

# 인자가 있는 경우 (출처: GeeksforGeeks)
class MyDecorator:
    def __init__(self, function):
        self.function = function
     
    def __call__(self):
        self.function()

 
# adding class decorator to the function
@MyDecorator
def function():
    print("GeeksforGeeks")
 
function()
# 인자가 있는 경우 (출처: GeeksforGeeks)
class MyDecorator:
    def __init__(self, function):
        self.function = function
     
    def __call__(self):
        self.function()

 
# adding class decorator to the function
@MyDecorator
def function():
    print("GeeksforGeeks")
 
function()

     2) __init__ and __call__ method

          * __init__ : python class의 생성자 처럼 동작하며, class 의 객체가 정의 될 때 동작

          * __call__ : python built in method 로써, Python 개발자가 함수처럼 동작하는 객체를 호출 할 때 사용

# 출처 : GeeksforGeeks
class Product:
    def __init__(self):
        print("Instance Created")
  
    # Defining __call__ method
    def __call__(self, a, b):
        print(a * b)
  
# Instance created
ans = Product()
  
# __call__ method will be called
ans(10, 20)

# Output
# Instance Created
# 200

 

4. 참고

(1) 개발 환경

      * Python 3.8.10

      * Django 3.2.4

      * Django Rest Framework 

 

(2) 참고 자료

https://stackoverflow.com/questions/7492068/python-class-decorator-arguments
 

Python class decorator arguments

I'm trying to pass optional arguments to my class decorator in python. Below the code I currently have: class Cache(object): def __init__(self, function, max_hits=10, timeout=5): self.

stackoverflow.com

 

https://www.geeksforgeeks.org/class-as-decorator-in-python/
 

Class as decorator in python - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

https://www.geeksforgeeks.org/__call__-in-python/
 

__call__ in Python - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

반응형