python, ECMAscript 등에서 decorator syntax를 지원하기 시작했지만, 그 개념이 아직은 널리 퍼지지 않아 생소한 사람들이 많을 것 같아 이 글을 작성한다.
이 글에서는 decorator 디자인 패턴에 대해 알아본 뒤, decorator 디자인 패턴과 프로그래밍 언어들에서 제공하는 decorator syntax의 관계를 살펴본다.
그리고, python, typescript와 같은 언어들에서 어떻게 decorator syntax를 지원하는지 각각 알아보고, C#, java 등의 사례도 함께 살펴본다.
데코레이터 패턴은 디자인 패턴 중 하나로, 본래 객체의 기능을 동적으로 확장시키는 패턴이다.
이 패턴은 하나의 객체에 추가해야 하는 기능들이 다양하고 복잡할 때에 빛을 발한다. 그 예시로
위와 같은 상황들이 있을 수 있다.
그럼 서브웨이 앱을 통해 고객의 주문을 비대면으로 받는다고 가정하고 decorator 패턴을 적용한 코드를 짜 보자.
다음과 같은 구조로 코드를 짜보려고 한다.
ISandwich
: 원본 객체와 데코레이터 객체에 모두 make()
가 존재하게 하여, 원본과 데코레이터 객체를 묶는 역할을 한다.Bread
: 원본 객체이다.VegetableDecorator
: Bread
를 장식할 역할을 맡는다.…Decorator
: 각 채소를 빵에 추가하는 로직을 구현한다.먼저 ISandwich
인터페이스와 Bread
원본 클래스를 만들어보자.
interface ISandwich {
void make();
}
class Bread implements ISandwich {
public void make() {
System.out.print("\n주문하신 샌드위치가 완성되었습니다: 플랫 브레드");
}
데코레이터를 활용해, 최종 실행 결과는 다음과 같도록 할 것이다.
샌드위치가 완성되었습니다: 플랫 브레드 + 피망 + 할라피뇨 + 올리브
그 다음으로 데코레이터 추상 클래스를 만들어보자.
abstract class VegetableDecorator implements ISandwich {
ISandwich bread;
VegetableDecorator(ISandwich bread) {
this.bread = bread;
}
public void make() {
bread.make();
}
}
VegetableDecorator는 bread
를 입력으로 받는데, 이를 통해 bread.make()
전후에 추가적인 로직을 끼워넣을 수 있다.
예를 들어, 피망을 추가해주는 PepperDecorator
를 다음과 같이 만들어보자.
class PepperDecorator extends VegetableDecorator {
PepperDecorator(ISandwich bread) {
super(bread);
}
public void make() {
System.out.print("안녕하세요, 서브웨이입니다.");
super.make();
System.out.print(" + 피망");
}
}
그리고 아래와 같이 실행 코드를 만들어 실행하면,
class SubwayEmployee {
public static void main(String[] args) {
new PepperDecorator(new Bread()).make();
}
}
아래와 같은 실행 결과가 출력된다.
안녕하세요, 서브웨이입니다.
샌드위치가 완성되었습니다: 플랫 브레드 + 피망
이와 같은 구조로 몇 개의 채소 종류를 더 추가하더라도, 채소 데코레이터를 새로 만들어 다양한 경우의 수를 커버할 수 있게 된다.
채소 종류가 하나 늘어날 때 그에 따라 증가하는 채소 조합의 경우의 수는 채소의 개수가 많아짐에 따라 기하급수적으로 증가하게 되는데, 이러한 문제점을 데코레이터 하나를 추가함으로써 해결할 수 있다는 사실은 굉장히 매력적으로 다가온다.
서브클래스의 구조가 여러 경우의 수로 복잡하게 나뉠 때, 데코레이터 패턴을 적용을 고려해 보는 것이 좋을 듯 하다.
class SubwayEmployee {
public static void main(String[] args) {
new Bread().make();
new OnionDecorator(new Bread()).make();
new PickleDecorator(new JalapenoDecorator(new Bread())).make();
new OliveDecorator(new JalapenoDecorator(new PepperDecorator(new Bread()))).make();
new PepperDecorator(new Bread()).make();
}
}
샌드위치가 완성되었습니다: 플랫 브레드
샌드위치가 완성되었습니다: 플랫 브레드 + 양파
샌드위치가 완성되었습니다: 플랫 브레드 + 할라피뇨 + 피클
샌드위치가 완성되었습니다: 플랫 브레드 + 피망 + 할라피뇨 + 올리브
샌드위치가 완성되었습니다: 플랫 브레드 + 피망
전체 코드를 아래 첨부한다.
https://gist.github.com/yechan2468/777edc39963d481b8092f803983fc872
흥미가 있다면 여기에 소스(sauce) 데코레이터를 추가 구현해 보자.
앞서 알아본 decorator 디자인 패턴과, 이후 알아볼 프로그래밍 언어에서의 decorator는 각기 다른 개념이다.
물론, decorator가 무언가를 래핑(wrapping)하고 기능을 추가하는 점이 decorater pattern과 유사해 decorator의 이름을 그렇게 지었을 수도 있었겠다는 점에서는 둘 사이에 아주 연관이 없지는 않겠으나, 둘 사이에는 명확히 구분이 필요하다.
실제로, decorator가 다른 언어에 비해 꽤 활발히 사용되는 python에서 처음으로 decorator 기능을 제안할 때 (PEP 318 - on the name decorator)에는 decorator라는 이름이 디자인 패턴에서 사용되는 decorator pattern과 매치가 되지 않는다며, decorator가 아닌 새로운 다른 이름을 사용할 수도 있음을 시사하기도 했다. decorator feature가 decorator pattern을 계승, 발전시킨 것이라기보다는, 별개의 것이라는 것이다.
python의 PEP 318에서는, “함수와 메서드를 변형하는 방법이 이상해서 코드를 읽기 어렵게 할 수 있다“며 다음과 같은 문제점을 제기했다.
def foo(cls):
# ...
# ...
# ...
# ...
# ...
# ...
pass
foo = synchronized(lock)(foo)
foo = classmethod(foo)
위와 같은 코드가 있다고 해보자. synchronized
, classmethod
에서는 함수 foo
를 인자로 받아, foo
가 추가적인 동작을 하도록 변형하고 있다.
이때, synchronized
와 classmethod
와 같은 것들은 함수 foo
의 동작을 바꾸는 중요한 일들을 하는반면, 이들은 문법 상 foo
가 모두 define된 이후에 나타날 수밖에 없다. 이렇게 되면 함수 foo
가 어떤 동작을 할지 쉽게 예측하기 어렵게 만든다.
이러한 문제는 특히 foo
의 몸체가 더 길어질 때 더 두드러지게 나타나며, 위와 같은 코드가 아래의 코드처럼 함수 선언부 근처에 위치할 수 있도록 하겠다는 것이 decorator syntax의 제안 이유이다.
@classmethod
@synchronized(lock)
def foo(cls):
pass
이 코드는 decorator syntax(@
)를 사용하지 않은 위 코드와 결과가 같다.
typescript에서 decorator를 만든 정확한 이유를 찾기는 어려웠다. 관심이 있는 분들은 tc39/proposal-decorators를 참고하자.
기존 함수, 변수를 변형하는 방법을 더욱 쉽게 하기 위함이라고 생각된다.
정리하면, decorator syntax는 기존에 함수의 합성 등 다른 방식으로도 구현이 가능했던 코드를 편의성, 가독성 면에서 증진시키기 위해 등장한 문법이라고 볼 수 있다.
python에서는 함수나 클래스를 대상으로 decorator syntax를 사용하여, 그들을 변형할 수 있다.
typescript에서는, 클래스, 클래스 필드, 클래스 메서드, 클래스 접근자를 대상으로 decorator syntax를 사용하여, 그들의 값을 변경, 접근 통제, 초기화할 수 있다.
그러나 앞서 소개했던 decorator 디자인 패턴에서는, 기존의 함수의 동작을 바꾸지 않은 채로, 그 함수의 실행 전후에 여러 가지 기능을 여러 경우의 조합으로 유연하게 끼워맞추는 데에 초점을 두었다.
decorator 디자인 패턴을 러시아의 마트료시카 인형으로 비유하고, decorator syntax를 납치범으로 비유할 수도 있을 것 같다.
이와 같이, decorator 디자인 패턴과 decorator syntax는 그 적용 대상과 적용 효과가 크게 다르다.
정리하면, decorator 디자인 패턴과 decorator syntax 모두, ‘어떠한 element를 어떻게 유연하게 변형시킬 수 있을까’라는 공통적인 문제를 해결하고자 고안된 것으로 보이고, 이에 따라 어느 정도 공통된 부분은 존재하는 듯 하다.
하지만, decorator 디자인 패턴은 클래스와 인터페이스를 활용해 프로젝트 구조 상의 유연성을 지원하는 것이 목적인 반면, decorator syntax는 그와 비교해 적용되는 범위도 다를 뿐더러, 프로젝트 구조가 아닌 함수, 클래스 등의 유연한 변형이 목적이며, 문법 상 지원되는 하나의 기능일 뿐이라는 차이점이 존재한다.
def pepper(fn):
def _pepper():
fn()
print(' + 피망', end='')
return _pepper
def onion(fn):
def _onion():
fn()
print(' + 양파', end='')
return _onion
def jalapeno(fn):
def _jalapeno():
fn()
print(' + 할라피뇨', end='')
return _jalapeno
@pepper
@onion
@jalapeno
def make():
print('샌드위치가 완성되었습니다: ', end='')
make()
# [실행 결과]
# 샌드위치가 완성되었습니다: 플랫 브레드 + 할라피뇨 + 양파 + 피망
python에서는 클래스와 함수에 decorator를 적용 가능하며, 각 decorator는 함수나 클래스를 인자로 받는 함수이다.
@
(pie)를 함수 혹은 클래스 선언부 앞에 적어 사용한다.
function pepper(fn: () => void, _context: ClassMethodDecoratorContext) {
return function () {
fn();
process.stdout.write(` + 피망`);
};
}
function onion(fn: () => void, _context: ClassMethodDecoratorContext) {
return function () {
fn();
process.stdout.write(` + 양파`);
};
}
function jalapeno(fn: () => void, _context: ClassMethodDecoratorContext) {
return function () {
fn();
process.stdout.write(` + 할라피뇨`);
};
}
class SubwayEmployee {
@pepper
@onion
@jalapeno
make() {
process.stdout.write(`샌드위치가 완성되었습니다: 플랫 브레드`);
}
}
new SubwayEmployee().make();
typescript의 decorator도 python에서의 decorator의 쓰임새와 유사하다.
typescript에서는 아래 요소들에 decorator를 적용할 수 있다.
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MyRepeatedAnnos.class)
@interface Words {
String word() default "Hello";
int value() default 0;
}
@Retention(RetentionPolicy.RUNTIME)
@interface MyRepeatedAnnos {
Words[] value();
}
public class Main {
@Words(word = "First", value = 1)
@Words(word = "Second", value = 2)
public static void newMethod()
{
Main obj = new Main();
try {
Class<?> c = obj.getClass();
Method m = c.getMethod("newMethod");
Annotation anno = m.getAnnotation(MyRepeatedAnnos.class);
System.out.println(anno);
} catch (NoSuchMethodException e) {
System.out.println(e);
}
}
public static void main(String[] args) { newMethod(); }
}
java의 annotation은 @
기호를 사용하고, 그 사용 방법이 python이나 typescript와 유사한 것처럼 보이기는 하지만, 그 동작 방식은 완전히 다르다.
annotation은 프로그램에 추가적인 정보를 제공해주는 메타데이터이며, annotate하는 코드에는 직접적인 영향을 주지 않는다.
아래 C#, PHP, Rust, F#의 attribute 또한 java의 annotation과 비슷한 구조를 가진다.
[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]
public class DeBugInfo : System.Attribute {
private int bugNo;
private string developer;
private string lastReview;
public string message;
public DeBugInfo(int bg, string dev, string d) {
this.bugNo = bg;
this.developer = dev;
this.lastReview = d;
}
public int BugNo { get { return bugNo; } }
public string Developer { get { return developer; } }
public string LastReview { get { return lastReview; } }
public string Message { get { return message; } set { message = value; } }
}
[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]
[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]
class Rectangle {
protected double length;
protected double width;
public Rectangle(double l, double w) {
length = l;
width = w;
}
[DeBugInfo(55, "Zara Ali", "19/10/2012", Message = "Return type mismatch")]
public double GetArea() {
return length * width;
}
[DeBugInfo(56, "Zara Ali", "19/10/2012")]
public void Display() {
Console.WriteLine("Length: {0}", length);
Console.WriteLine("Width: {0}", width);
Console.WriteLine("Area: {0}", GetArea());
}
}
#[FooAttribute]
function foo_func(#[FooParamAttrib('Foo1')] $foo) {}
#[FooAttribute('hello')]
#[BarClassAttrib(42)]
class Foo {
#[ConstAttr]
#[FooAttribute(null)]
private const FOO_CONST = 28;
private const BAR_CONST = 28;
#[PropAttr(Foo::BAR_CONST, 'string')]
private string $foo;
#[SomeoneElse\FooMethodAttrib]
public function getFoo(#[FooClassAttrib(28)] $a): string{}
}
#[Attribute]
class FooAttribute {
public function __construct(?string $param1 = null) {}
}
#[Attribute]
class ClassAttrib {
public function __construct(int $index) {}
}
#![crate_type = "lib"]
#[test]
fn test_foo() {
/* ... */
}
#[cfg(target_os = "linux")]
mod bar {
/* ... */
}
#[allow(non_camel_case_types)]
type int8_t = i8;
fn some_unused_variables() {
#![allow(unused_variables)]
let x = ();
let y = ();
let z = ();
}
open System
[<Obsolete("Do not use. Use newFunction instead.")>]
let obsoleteFunction x y =
x + y
let newFunction x y =
x + 2 * y
let result1 = obsoleteFunction 10 100
let result2 = newFunction 10 100
데코레이터(Decorator) 패턴 - 완벽 마스터하기
A simple explanation of decorators in Javascript
A Gentle Introduction to Decorators in Python
Decorator Pattern and Python Decorators
PEP 318 – Decorators for Functions and Methods
What are decorators and how are they used in JavaScript ?
What’s New in ****TypeScript 5.0
JavaScript metaprogramming with the 2022-03 decorators API
Anatomy of TypeScript “Decorators” and their usage patterns
Exploring EcmaScript Decorators
Python’s decorators vs Java’s annotations, same thing?
The Rust Reference - Attributes
Our Guide to Python Decorators
[How to decorate a class?](https://stackoverflow.com/questions/681953/how-to-decorate-a-class)
[proposal-decorators](https://github.com/tc39/proposal-decorators)
정리가 잘 된 글이네요. 도움이 됐습니다.