이 글은 decorator(@기호와 함께 쓰는)라고 하는 파이썬의 기능에 관한 것이 아니라 "데코레이터" 및 "위임"라는 디자인 패턴에 관한 글이다.
이전 글에 이어서 이번에는 위임을 사용하는 데코레이터 패턴에 대해 정리해보고자 한다.
참고) 공부중에 나는 “파이썬의 데코레이터가 데코레이터 패턴을 구현한 것과 동일한 것이 아닌가?” 라는 의문점이 있었는데 이 글을 보고 차이점을 이해하였다.
데코레이션(decoration)이란?
- 객체에 기능을 추가하고는 싶지만 해당 객체의 클래스를 확장하지 않을 때 사용
- 기본 클래스에 영향을 주지 않고 필요할 때 동작을 동적으로 추가
- 객체 지향 설계의 단일 책임 원칙(SRP)을 준수를 유지하는 데 필요
- 위임과 마찬가지로 데코레이터 패턴은 상속으로 문제를 해결할 수 있지만 실제로 디자인 측면에서 이치에 맞지 않거나 실행 불가능한 경우에 자주 사용
예시 - HR 시스템
간단한 인사 시스템을 예시로 들어보자.
생성할 클래스는 다음과 같다.
- 직원용 클래스
- 연도별(YTD) 급여 보고서용 클래스
이 보고서에는 연초부터 지금까지 직원이 받은 급여가 나열된다.
from datetime import datetime
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class SalaryYTDReport:
def __init__(self, employees):
self.employees = employees
self.data = []
def prep_data(self):
self.data = []
for employee in self.employees:
emp_sal_dict = {'Employee': employee.name, 'YTD Salary': self.calculate_YTD_salary(employee.salary)}
self.data.append(emp_sal_dict)
def calculate_YTD_salary(self, salary):
current_month = datetime.today().month
ytd_salary = (salary/12) * current_month
ytd_salary = round(ytd_salary, 2)
return ytd_salary
SalaryYTDReport
는 직원 사전 배열을 생성하며, 우리의 목표는 이 정보를 잘 보여주는 것이다. 정보를 보여주는 데에는 터미널, 웹 페이지, 스프레드 시트 등 다양한 옵션이 있지만 이러한 다양한 형태로 데이터를 표시하는 기능은 이 클래스의 책임이라고 볼 수 없다.
이 클래스는 이름 그대로 데이터를 수집하고 보고서와 같은 형식으로 데이터를 구조화하는 역할만을 해야한다.
대안
만약 상속이 유일한 대안이라면, 보고서 표시 메서드를 포함하는 Reporting
클래스를 생성하여 SalaryYTDReport
가 상속받도록 하는 것이 적절하다고 생각될 것이다. 또는 데이터의 형식만SalaryYTDReport
에서 지정해주고, 이를 상속하는 Reporting
클래스를 만들어야 한다고 생각할 수도 있다.
문제점
이러한 방법은 소규모 또는 중간 규모의 운영에 적합할 수 있다. 하지만 회사에 월 단위 급여가 아닌 시간당으로 일하는 직원이 있을 수 있다. 여러 부서에서 각자의 보고서 표시 방법을 구현하고 싶을 경우, 이 코드는 활용될 수 없다. 이런 경우 데코레이터를 도입해야한다.
Report 데코레이터
SalaryYTD
클래스의 SRP
를 유지하려면 이 클래스의 유일한 책임은 YTD 급여 데이터를 보고하는 것뿐이어야하며, 이 클래스에 기능을 추가하려면 클래스 이름을 변경해야 한다. 물론 이게 나쁜 것은 아니지만, 현재 클래스가 하고 있는 기능보다 더 많은 기능을 계속해서 추가하는 것은 바람직하지 않다.
보고서 클래스(여기서는 SalaryYTDReport 클래스)에 기본적으로 보고하는 기능을 추가하는 클래스를 만들어보자. 우리는 이 보고서가 웹 페이지에서 HTML로만 표시된다는 점만 알고있지만, 중간에서 컨트롤하는 레이어는 보고서 데이터로 다른 작업을 수행한다고 가정한다. 이 경우 SalaryYTDReport
클래스의 기능을 유지하면서 보고서를 HTML로 표시하는 메서드를 제공하는 클래스가 필요하다.
이때 위임에 대해 배운 내용이 유용하게 쓰일 수 있다. 보고서를 HTML로 표시하는 기능을 추가하면서 SalaryYTDReport
의 기본 기능을 위임하는 데코레이터 클래스를 만들 것이다. 즉, HTML 서식 지정 기능으로 SalaryYTDReport
클래스를 장식(decorate)할 것이다.
이때 이전 글에서와 같은 위임 접근 방식을 사용하지만 매우 중요한 차이점이 있다. SalaryYTDReport
클래스에서 prep_data()
및 data
에 모두 접근하고자 한다. 1부에서 사용한 접근 방식의 문제점은 속성(attribute)이 아닌 클래스 메서드에서만 작동한다는 것이다. 다시 말해, 1부에서 했던 것과 똑같은 방식으로 이 문제에 접근하면 report.data에서 오류가 발생한다.
이 문제를 해결하기 위해 파이썬에 내장된 __dict__
를 사용하여 속성을 파악하고, 기존의 dir
접근 방식을 사용하여 메서드를 파악하도록 한다. 그런 다음 __getattr__
재작성을 조금만 수정하도록 한다.
아래와 같이 메서드의 경우는 메서드를 반환하고 호출하고, 속성일 경우는 요청된 속성을 반환하기만 하면 된다.
class HTMLReportDecorator:
def __init__(self, report):
self.html_report = []
self.report = report
self.report_methods = [f for f in dir(SalaryYTDReport) if not f.startswith('_')]
self.report_attributes = [a for a in report.__dict__.keys()]
def __getattr__(self, func):
if func in self.report_methods:
def method(*args):
return getattr(self.report, func)(*args)
return method
elif func in self.report_attributes:
return getattr(self.report, func)
else:
raise AttributeError
def report_data(self):
self.html_report = []
self.prep_data()
for row in self.data:
name = f"<b>{row['Employee']}</b>"
ytd = f"<i>{row['YTD Salary']}</i>"
html_row = f"{name}: {ytd}<br />"
self.html_report.append(html_row)
이제 SalaryYTDReport
객체를 생성하고, 이 객체를 사용하여 HTMLReportDecorator
의 인스턴스를 인스턴스화하여 장식한 다음, SalaryYTDReport
를 인스턴스화할 때 사용한 것과 동일한 변수에 할당한다.
이런식으로하면 데코레이트된 객체를 몇 가지 추가 기능이 있는 베이스 객체처럼 사용할 수 있다. 이 객체는 기본 클래스 자체(이 경우 SalaryYTDReport
)와 똑같이 작동하면서 데코 클래스의 기능을 가진다.
>>> from report import *
>>> emp1 = Employee('Bob', 100000)
>>> emp2 = Employee('Jan', 150000)
>>> emp3 = Employee('Erik', 30)
>>> report = SalaryYTDReport([emp1, emp2, emp3])
>>> report = HTMLReportDecorator(report)
>>> # 베이스 객체 SalaryYTDReport 처럼 사용:
>>> for employee in report.employees:
... print(employee.name)
Bob
Jan
Erik
>>> # Show it has the decorator's functionality too:
>>> report.report_data() # 리포트 생성
>>> report.html_report # 리포트 표현
['<b>Bob</b>: <i>66666.67</i><br />', '<b>Jan</b>: <i>100000.0</i><br />', '<b>Erik</b>: <i>20.0</i><br />']
예시 2: 로깅 메서드
앞의 예시는 클래스를 다시 작성하지 않고도 기본적으로 클래스에 기능을 추가하는 데코레이터 패턴의 매우 기본적인 구현을 보여준다. 이번에는 조금 더 복잡한 데코레이터 메서드의 사용 사례를 구현해 보도록 한다.
어떤 이유로 메서드의 시작과 끝을 로깅하고 싶다고 가정해 보자. 이는 향후 사용자 행동을 분석하거나 성능 인사이트를 수집하는 등 다양한 이유로 수행될 수 있다. 후자를 위한 솔루션을 구현해 보자.
모든 메서드 호출의 시간을 기록하는 SalaryYTDReport
의 데코레이터를 작성할 것이다.
import time
...
class PerformanceLogReportDecorator:
def __init__(self, report):
self.report = report
self.report_methods = [f for f in dir(SalaryYTDReport) if not f.startswith('_')]
self.report_attributes = [a for a in report.__dict__.keys()]
def __getattr__(self, func):
if func in self.report_methods:
def method(*args):
return self.log(func, *args)
return method
elif func in self.report_attributes:
return getattr(self.report, func)
else:
raise AttributeError
def log(self, func, *args):
start = datetime.now()
getattr(self.report, func)(*args)
time.sleep(1) # 로깅이 실제로 일어나고있음을 보여주기 위한 용도
end = datetime.now()
microseconds = (end-start).microseconds
print(f"{func} ran in {microseconds} microseconds")
보고서 클래스와 마찬가지로 사용하되, PerformanceLogReportDecorator
클래스로 장식하기 때문에 로깅이라는 보너스 기능을 얻을 수 있다.
>>> from report import *
>>> emp1 = Employee('Bob', 100000)
>>> emp2 = Employee('Jan', 150000)
>>> emp3 = Employee('Erik', 30)
>>> report = SalaryYTDReport([emp1, emp2, emp3])
>>> report = PerformanceLogReportDecorator(report)
>>> report.prep_data()
prep_data ran in 709 microseconds
실제 사용할 상황에서는 콘솔에 출력하지 않고 로그 파일이나 데이터베이스에 기록하게 될 것이다.
결론
대부분의 디자인 패턴과 마찬가지로, 문제는 구현에 있는 것이 아니라 언제 사용해야 하는지 인식하는 데 있는 것 같다.
'Python' 카테고리의 다른 글
[디자인패턴] 파이썬에서 Delegate Pattern (위임 패턴) 구현하기 (1) | 2023.05.07 |
---|---|
[Python/NLP] 위키피디아 덤프 데이터에서 하이퍼링크(anchor text) 추출하기 (0) | 2021.12.06 |
[Python] Beutiful Soup4 - decompose() 와 extract() (0) | 2021.10.01 |
[Python] 백준 알고리즘 5단계: 1차원 배열 (0) | 2021.09.11 |