Python 데코레이터 패턴
Python 데코레이터 패턴
데코레이터 패턴(Decorator Pattern)이란?
데코레이터 패턴은 객체의 기본 기능에 추가적인 동작을 동적으로 더하는 데 사용되는 구조적 디자인 패턴입니다. 이 패턴을 사용하면 기존의 코드를 수정하지 않고도 새로운 기능을 쉽게 추가할 수 있습니다. 주로 열려 있는 폐쇄 원칙(Open-Closed Principle)을 따르는 데코레이터 패턴은 코드 확장이 용이하면서도 기존 구현을 안전하게 유지합니다.
열려 있는 폐쇄 원칙(Open-Closed Principle, OCP)은 객체 지향 설계의 SOLID 원칙 중 하나로, 소프트웨어 시스템을 설계할 때 다음을 목표로 합니다:
- 확장에는 열려 있어야(Open): 새로운 기능이나 동작을 추가할 수 있도록 설계되어야 한다.
- 수정에는 닫혀 있어야(Closed): 기존의 코드(클래스, 메서드 등)를 수정하지 않아야 한다.
데코레이터 패턴을 사용하는 이유
- 코드 재사용성 증가: 여러 객체에 공통적인 부가 기능을 쉽게 적용할 수 있습니다.
- 유연성 향상: 기존 클래스나 객체를 변경하지 않고도 새로운 행동을 추가할 수 있습니다.
- 구조적 깔끔함: 상속을 사용하는 것보다 가독성과 유지보수성이 뛰어납니다.
- 단일 책임 원칙(SRP) 준수: 각각의 데코레이터가 독립적인 기능 단위로 유지됩니다.
데코레이터 패턴을 사용해야 하는 Case
데코레이터 패턴을 사용해야 하는 경우는 기존 객체의 기본 동작을 변경하지 않으면서 새로운 기능을 추가해야 하는 경우입니다. 특히, 새로운 요구사항이나 기능이 자주 추가되거나 다양한 조합으로 기능을 적용해야 할 때 유용합니다. 아래는 데코레이터 패턴을 사용하는 구체적인 경우입니다.
1. 기존 코드를 수정할 수 없는 경우
- 외부 라이브러리나 서드파티에서 제공된 클래스를 직접 수정할 수 없을 때, 동작을 확장하거나 수정하고 싶다면 데코레이터 패턴이 적합합니다.
- 기존 클래스를 감싸는 데코레이터를 통해 기능을 동적으로 추가할 수 있습니다.
예시: 외부 라이브러리에서 제공되는 Logger
클래스의 기본 출력 형식을 수정하지 않고, 로그에 타임스탬프를 추가하는 데코레이터를 작성할 수 있습니다.
2. 기능 확장이 필요하지만 상속은 피하고 싶을 때
- 상속은 자식 클래스의 폭발적인 증가를 초래할 수 있습니다(모든 조합에 대해 새로운 클래스를 작성해야 함). 데코레이터 패턴은 조합을 통해 동적 확장을 가능하게 합니다.
- 상속 대신 구성(Composition)을 활용하여 확장성을 높이고 유지보수를 용이하게 만듭니다.
예시: 문서를 렌더링하는 클래스에 대해, PDF 렌더링과 HTML 렌더링 기능을 추가해야 할 때, 상속 대신 데코레이터를 사용하여 각 기능을 추가로 제공할 수 있습니다.
3. 기능이 독립적이고 유연하게 추가/제거되어야 할 때
- 애플리케이션에서 다양한 기능을 상황에 따라 필요한 만큼만 조합하여 사용해야 할 때, 데코레이터 패턴이 적합합니다.
- 필요 없는 기능은 제외하고, 필요한 기능만 선택적으로 적용 가능합니다.
예시: 알림 시스템에서 이메일, SMS, 푸시 알림 등을 상황에 따라 조합하여 사용할 수 있습니다.
4. 객체의 동작 전후에 부가적인 작업이 필요할 때
- 객체의 동작을 실행하기 전이나 후에 추가 작업을 수행해야 한다면, 데코레이터 패턴이 적합합니다.
- 이 경우 여러 데코레이터를 연결하여 단계별로 처리 로직을 확장할 수 있습니다.
예시: 웹 요청을 처리하기 전, 인증/권한 확인, 로깅, 캐싱 등을 단계적으로 수행하는 HTTP 미들웨어.
5. 동일한 기능을 여러 객체에 적용해야 할 때
- 동일한 부가 기능(로깅, 캐싱, 검증 등)을 여러 객체에 추가해야 한다면, 데코레이터 패턴을 사용하여 코드를 재사용할 수 있습니다.
- 객체에 독립적으로 적용 가능하며, 특정 객체에만 기능을 선택적으로 적용할 수도 있습니다.
예시: 데이터베이스 연산 클래스와 파일 입출력 클래스 모두에 동일한 로깅 기능을 추가하려면, 공통 데코레이터를 작성하여 두 클래스에 적용할 수 있습니다.
6. 런타임에 객체의 동작을 동적으로 변경해야 할 때
- 런타임에 객체에 추가적인 동작을 더하거나 제거해야 하는 경우, 데코레이터 패턴이 적합합니다.
- 프로그램 실행 중 기능을 조합하거나 제거하는 것이 용이합니다.
예시: 게임에서 캐릭터가 아이템을 획득하면 새로운 능력을 부여하거나 기존 능력을 확장하도록 설계.
데코레이터 패턴 사용 사례 요약
사용 상황 | 예시 |
---|---|
기존 클래스를 수정할 수 없지만 기능 추가 필요 | 외부 라이브러리 객체 확장 |
상속 대신 조합을 통해 기능 확장 | 알림 시스템: 이메일 + SMS + 푸시 알림 |
독립적인 기능 추가/제거 필요 | 웹 요청 처리: 로깅, 인증, 캐싱 |
동일한 기능을 여러 객체에 적용 | 데이터베이스와 파일 처리에서 공통 로깅 |
동작 전후에 작업 추가 필요 | 요청 처리 전 인증, 작업 후 리소스 해제 |
런타임에 동적 기능 조합/변경 | 게임 캐릭터 능력 추가/제거 |
데코레이터 패턴의 한계와 대안
- 구현 복잡도 증가: 데코레이터가 많아지면, 클래스 계층이 깊어져 관리가 어려울 수 있습니다.
- 디버깅 어려움: 중첩된 데코레이터로 인해 문제의 원인을 파악하기 어려울 수 있습니다.
대안으로는 단순한 경우 전략 패턴(Strategy Pattern)이나 고차 함수를 사용할 수도 있습니다. 그러나 확장성과 유연성이 중요한 경우 데코레이터 패턴이 여전히 강력한 도구입니다.
데코레이터 패턴의 실제 사용 사례
예제: 이메일 알림 시스템 확장하기
아래는 기본 이메일 전송 기능을 가진 클래스에 SMS 전송과 푸시 알림 기능을 동적으로 추가하는 데코레이터 패턴의 예제입니다.
# 기본 EmailSender 클래스
class EmailSender:
def send(self, message: str):
print(f"Sending email: {message}")
# 데코레이터 기본 클래스
class NotificationDecorator:
def __init__(self, sender):
self.sender = sender
def send(self, message: str):
self.sender.send(message)
# SMS 알림 데코레이터
class SMSDecorator(NotificationDecorator):
def send(self, message: str):
super().send(message)
print(f"Sending SMS: {message}")
# 푸시 알림 데코레이터
class PushNotificationDecorator(NotificationDecorator):
def send(self, message: str):
super().send(message)
print(f"Sending push notification: {message}")
# 실제 사용
if __name__ == "__main__":
# 기본 이메일 전송 기능
= EmailSender()
email_sender
# 이메일 + SMS 전송 기능 추가
= SMSDecorator(email_sender)
email_with_sms
# 이메일 + SMS + 푸시 알림 기능 추가
= PushNotificationDecorator(email_with_sms)
full_notification
# 메시지 전송
print("=== 기본 이메일 전송 ===")
"Hello, this is a basic email!")
email_sender.send(
print("\n=== 이메일 + SMS 전송 ===")
"Hello, this includes SMS notification!")
email_with_sms.send(
print("\n=== 이메일 + SMS + 푸시 알림 전송 ===")
"Hello, this includes all notifications!") full_notification.send(
실행 결과
=== 기본 이메일 전송 ===
Sending email: Hello, this is a basic email!
=== 이메일 + SMS 전송 ===
Sending email: Hello, this includes SMS notification!
Sending SMS: Hello, this includes SMS notification!
=== 이메일 + SMS + 푸시 알림 전송 ===
Sending email: Hello, this includes all notifications!
Sending SMS: Hello, this includes all notifications!
Sending push notification: Hello, this includes all notifications!
위 코드는 데코레이터 패턴을 사용하여 이메일 전송 시스템에 동적으로 추가 기능(SMS 및 푸시 알림)을 부여하는 구조를 보여줍니다. 각 부분을 하나씩 살펴보겠습니다.
1. 기본 EmailSender
클래스
class EmailSender:
def send(self, message: str):
print(f"Sending email: {message}")
- 역할: 이메일을 전송하는 기본 기능을 제공합니다.
- 메서드:
send(message: str)
: 이메일 메시지를 출력하는 메서드입니다.
- 이 클래스는 단순히 이메일을 전송하는 기능만 수행하며 확장성이 없습니다.
2. NotificationDecorator
(데코레이터 기본 클래스)
class NotificationDecorator:
def __init__(self, sender):
self.sender = sender
def send(self, message: str):
self.sender.send(message)
- 역할: 모든 데코레이터 클래스가 상속받는 기본 데코레이터 클래스입니다.
- 동작 원리:
- 초기화 시, 데코레이터가 꾸밀 대상 객체(
sender
)를 저장합니다. send
메서드는 꾸밀 대상 객체의send
메서드를 호출합니다.
- 초기화 시, 데코레이터가 꾸밀 대상 객체(
- 이 클래스를 직접 사용하지 않고, 확장된 데코레이터 클래스가 실제 동작을 정의합니다.
3. SMSDecorator
(SMS 알림 데코레이터)
class SMSDecorator(NotificationDecorator):
def send(self, message: str):
super().send(message)
print(f"Sending SMS: {message}")
- 역할: 기본
send
동작에 SMS 알림 기능을 추가합니다. - 동작 원리:
super().send(message)
: 꾸밀 대상 객체의send
메서드를 먼저 호출합니다.print(f"Sending SMS: {message}")
: 메시지를 SMS로 전송(출력)하는 추가 동작을 수행합니다.
4. PushNotificationDecorator
(푸시 알림 데코레이터)
class PushNotificationDecorator(NotificationDecorator):
def send(self, message: str):
super().send(message)
print(f"Sending push notification: {message}")
- 역할: 기본
send
동작에 푸시 알림 기능을 추가합니다. - 동작 원리:
super().send(message)
: 이전 데코레이터 또는 원래 객체의send
메서드를 호출합니다.print(f"Sending push notification: {message}")
: 메시지를 푸시 알림으로 전송(출력)하는 추가 동작을 수행합니다.
5. 실제 사용
if __name__ == "__main__":
# 기본 이메일 전송 기능
= EmailSender()
email_sender
# 이메일 + SMS 전송 기능 추가
= SMSDecorator(email_sender)
email_with_sms
# 이메일 + SMS + 푸시 알림 기능 추가
= PushNotificationDecorator(email_with_sms) full_notification
동작 흐름:
email_sender
는 이메일 전송 기능만을 제공하는 객체입니다.SMSDecorator
는email_sender
를 꾸며서 이메일 + SMS 전송 기능을 제공합니다.PushNotificationDecorator
는email_with_sms
를 꾸며서 이메일 + SMS + 푸시 알림 전송 기능을 제공합니다.
6. 메시지 전송 시 동작
(1) 기본 이메일 전송
"Hello, this is a basic email!") email_sender.send(
호출된 메서드:
EmailSender.send
출력:
Sending email: Hello, this is a basic email!
(2) 이메일 + SMS 전송
"Hello, this includes SMS notification!") email_with_sms.send(
호출된 메서드:
SMSDecorator.send
→EmailSender.send
출력:
Sending email: Hello, this includes SMS notification! Sending SMS: Hello, this includes SMS notification!
(3) 이메일 + SMS + 푸시 알림 전송
"Hello, this includes all notifications!") full_notification.send(
호출된 메서드:
PushNotificationDecorator.send
→SMSDecorator.send
→EmailSender.send
출력:
Sending email: Hello, this includes all notifications! Sending SMS: Hello, this includes all notifications! Sending push notification: Hello, this includes all notifications!
핵심 개념 정리
- 데코레이터 패턴의 장점:
- 기존 코드를 수정하지 않고도 새로운 기능을 동적으로 추가 가능.
- 데코레이터를 조합하여 다양한 기능 조합 생성 가능.
- 상속 대신 구성(Composition)을 사용하여 더 유연한 구조 제공.
- 동작 원리:
- 데코레이터 클래스가 원래 객체를 감싸고, 추가 동작을 정의하며, 필요 시 원래 객체의 동작을 호출합니다.
이 패턴은 동적인 기능 확장이 필요한 경우에 매우 유용하게 사용됩니다!
데코레이터 패턴을 활용한 장점
- 기능 추가 및 제거가 쉬움: 원하는 데코레이터를 추가하거나 제거하는 방식으로 조합이 가능합니다.
- 확장 가능한 설계: 새로운 알림 방식(SNS, 팩스 등)을 추가하려면 단순히 새 데코레이터 클래스를 작성하면 됩니다.
- 상속의 대안: 기능 확장을 위해 복잡한 다중 상속을 피할 수 있습니다.
Python의 내장 데코레이터와 비교
Python에서는 데코레이터가 함수나 메서드의 동작을 수정할 때도 사용됩니다. 예를 들어, @staticmethod
나 @classmethod
와 같은 내장 데코레이터를 사용하면 함수에 새로운 동작을 동적으로 추가할 수 있습니다.
결론
데코레이터 패턴은 유연한 코드 확장과 간결한 구조를 제공하여 유지보수를 단순화하고, 객체 지향 설계의 기본 원칙을 잘 지킬 수 있게 해주는 강력한 도구입니다.