플라스크와 장고같은 웹 프레임워크에서 함수 위에 @ 가 선언되어 있는 것을 자주 볼 수 있다. 이는 데코레이터(decorator) 라고 하는 문법이다.
from django.contrib.auth.decorators import login_required
@login_required
def my_view(request):
method 데코레이터는 사용자가 구조를 수정하지 않고 기존 객체에 새로운 기능을 추가할 수 있도록 하는 python의 디자인 패턴이다.
데코레이터를 쓰면 성능상 나아지는 것은 없지만, 간결한 코드로 시스템 관리가 수월해진다.
def decorator(func):
def 내부함수이름(*args, **kwargs):
기존 함수에 추가할 명령
return func(*args, **kwargs)
return 내부함수이름
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):
...
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 는 파이썬에서 객체지향 프로그래밍을 지원하는 데코레이터의 일종이다. 외부에서 클래스 내부 변수를 참조하기 위한 함수이다. 흔히 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() }} 와 같이 불러들여 필드를 불러오는 느낌을 낼 수 없었을 것이다.