안녕하세요.
글쓰는 개발자 입니다.
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 가 불필요하게 검증 로직을 통과
(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
https://www.geeksforgeeks.org/class-as-decorator-in-python/
https://www.geeksforgeeks.org/__call__-in-python/