(python) 편리한 데코레이터

duo2208·2022년 1월 24일
0

Language

목록 보기
1/3
post-thumbnail

플라스크와 장고같은 웹 프레임워크에서 함수 위에 @ 가 선언되어 있는 것을 자주 볼 수 있다. 이는 데코레이터(decorator) 라고 하는 문법이다.

from django.contrib.auth.decorators import login_required
 
@login_required
def my_view(request):    

데코레이터(decorator) 란 ?


method 데코레이터는 사용자가 구조를 수정하지 않고 기존 객체에 새로운 기능을 추가할 수 있도록 하는 python의 디자인 패턴이다.

  • 함수를 인수로 얻고, 대가로 새로운 함수로 돌려주는 callable()
  • 어떤 함수의 내부를 수정하지 않고 기능에 변화를 주고 싶을 때 사용
  • 즉 함수를 감싸고 있는 함수.

데코레이터를 쓰면 성능상 나아지는 것은 없지만, 간결한 코드로 시스템 관리가 수월해진다.

데코레이터의 구조

def decorator(func):
	def 내부함수이름(*args, **kwargs):
    	기존 함수에 추가할 명령
        return func(*args, **kwargs)
    return 내부함수이름



데코레이터(decoreator), 어디에 사용될까 ?


  • 유효성 검사와 런타임 검사

python 의 타입 체계는 타입에 엄격하지만(strongly typed) 매우 동적이다. 이 모든 이점에도 불구하고 python 의 이런 특성 때문에 Java 같은 정적 타입 언어라면 컴파일 시점에서 포착했을 법한 버그가 생길 수 있다.
하지만 시야를 좀 더 넓히면 들어오고 나가는 데이터에 대해 좀 더 세련된 맞춤형 검사를 강제하고 싶을 수도 있습다. 데코레이터를 이용하면 이 모든 작업을 손쉽게 처리하고 한번에 여러 함수에 적용할 수 있다.

한번 상상해 보자. 함수가 여럿 있고, 각 함수는 딕셔너리를 하나 반환하는데, 이 딕셔너리에는 다른 필드와 함께 summary 라는 필드가 포함되어있다. 이 요약값은 80자를 넘으면 안 도니다. 이를 위반하면 오류다. 다음은 이 같은 오류가 발생할 경우 ValueError 를 던지는 데코레이터이다.

def validate_summary(func):
    def wrapper(*args, **kwargs):

        data = func(*args, **kwargs)
        if len(data["summary"]) > 80:
            raise ValueError("Summary too long")
        return data

    return wrapper

@validate_summary
def fetch_customer_data():
    # ...

@validate_summary
def query_orders(criteria):
    # ...
  • 프레임워크 제작

데코레이터를 작성하는 법에 익숙해지고 나면 데코레이터를 사용하는 단순한 문법의 이점을 얻을 수 있다. 바로 사용하기 쉬운 언어에 시맨틱을 추가할 수 있다는 것이다. 그다음으로 가장 좋은 것은 python 자체의 문법을 확장할 수 있다는 것이다.

여러 인기 있는 오픈소스 프레임워크에서는 데코레이터를 사용하고 있다. 장고에선 스태프 멤버만이 조회할 수 있는 페이지를 다음과 같이 데코레이터만으로도 간단하게 구현할 수 있다.

from django.contrib.admin.views.decorators import staff_member_required

@staff_member_required
def my_view(request):
    ...
🚀 (Django) staff_member_required 데코레이터
  • 재사용 불가능한 코드의 재사용

python 에서는 표현력 있는 함수 문법, 함수형 프로그래밍 지원, 완전한 기능의 객체 시스템을 통해 코드를 손쉽게 재사용할 수 있는 형태로 캡슐화하는 강력한 도구를 제공한다. 하지만 이것만으로는 처리할 수 없는 코드 재사용 패턴이 있다.

불안정한 API를 이용하고 있다고 해보자. HTTP를 통해 JSON 형식으로 요청을 보내면 99.9%의 경우에는 올바르게 동작한다. 하지만 아주 일부 요청이 서버에서 내부 오류를 일으켜서 요청을 재시도해야 한다. 이 경우 다음과 같은 재시도 로직을 구현할 것이다.

resp = None
while True:
    resp = make_api_call()
    if resp.status_code == 500 and tries < MAX_TRIES:
        tries += 1
        continue
    break
process_response(resp)

이제 make_api_call() 같은 함수가 여러 개 있고 이러한 함수를 코드 곳곳에서 호출한다고 상상해 보자. 함수를 호출하는 while 반복문을 그때그때마다 구현해야 하나? 새로운 API 호출 함수를 추가할 때마다 같은 작업을 반복해야 하나? 이 같은 패턴은 반복 작성 코드(boilerplate code)를 만들기가 어렵다. 데코레이터를 사용하지 않는다면 말이다. 데코레이터를 사용하면 문제가 상당히 간단해진다.

# 데코레이터가 적용된 함수에서는 Response 객체를 반환하고
# 이 객체에는 status_code 속성이 포함돼 있습니다.
# 200은 성공을 의미하고, 500은 서버 측 오류를 의미합니다.

def retry(func):
    def retried_func(*args, **kwargs):
        MAX_TRIES = 3
        tries = 0
        while True:
            resp = func(*args, **kwargs)
            if resp.status_code == 500 and tries < MAX_TRIES:
                tries += 1
                continue
            break
        return resp
    return retried_func

이렇게 하면 사용하기 쉬운 @retry 데코레이터가 만들어집니다.

@retry
def make_api_call():
    # ....



@property 란 ?


@property 는 파이썬에서 객체지향 프로그래밍을 지원하는 데코레이터의 일종이다. 외부에서 클래스 내부 변수를 참조하기 위한 함수이다. 흔히 getter, setter 라고도 이야기하기도 한다.

@property 는 메소드를 마치 필드명을 사용하는 것처럼 깔끔하게 호출할 수 있게 해준다. 필드명처럼 사용하면 코드가 간결하며 읽기 편하게 되어 안정적인 인터페이스를 제공할 수 있다.

+ 예제로 이해하기

다음은 장고에서 회원가입시에 유저가 프로필이미지를 업로드 했으면 해당 이미지를 프로필로 제공하고, 이미지를 업로드 하지 않았으면 django-pydenticon 라이브러리에서 제공하는 랜덤이미지를 프로필로 제공하기 위한 코드이다.

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.shortcuts import resolve_url
from django.db import models

class User(AbstractUser):
    ...
    avatar = models.ImageField(blank=True, upload_to='accounts/profile/%Y/%m/%d')
    
    @property
    def name(self):
        return f"{self.first_name} {self.last_name}"

    @property
    def avatar_url(self):
        if self.avatar:
            return self.avatar.url
        else:
            return resolve_url("pydenticon_image", self.username)
# sns/models.py
from accounts.models import settings
from django.db import models

class Post(models.Model): 
    author = models.ForeignKey(settings.AUTH_USER_MODEL, \
    	related_name='my_post_set', on_delete=models.CASCADE)
    ...
# templates/profile.html
...
<img src="{{ post.author.avatar_url }}" />
...

@property 를 사용하지 않았다면 {{ post.author.avatar_url() }} 와 같이 불러들여 필드를 불러오는 느낌을 낼 수 없었을 것이다.


📌 참고 출처

0개의 댓글