Javascript 장식자(decorator) 패턴

목이·2019년 12월 8일
7

기법

목록 보기
1/2
post-thumbnail

ES2015 부터 자바스크립트는 클래스 문법을 직접 지원합니다. 클래스 상속을 위해 extends 란 명시적인 키워드가 제공되고, 생성자 안에서 super 메서드 호출이 가능해졌습니다. 무엇보다 반가운건 클래스 내부 어디에서 사용하든 this 는 현재 인스턴스를 참조하도록 제공된다는 점입니다. 그동안 자바스크립트는 의사(pseudo) 클래스를 이용해 프로토타입 체인 구조안에서 변칙스러운 this 키워드를 고려해가며 작업해야했지만 이제는 보다 명확한 문법으로 구현할 수 있게 되었습니다. 이번에 소개할 장식자 패턴의 구성요소들은 ES2015 문법으로 작성되었습니다.

장식자(Decorator) 패턴

GOF 의 디자인패턴 에서 언급했듯이 장식자(Decorator) 패턴은 객체에 새로운 행동을 추가하는 가장 효과적인 방법입니다. 새로운 행동이 필요해서 이를 구현한 정적 클래스를 추가로 생성하는 것은 유연하지 못한 설계입니다. 그보다 필요한 시점에 새로운 행동을 동적으로 장식할 수 있다면 클래스 계통의 상부측 클래스에 많은 책임과 기능이 누적되는 것을 피할 수 있습니다.

Decoratable 믹스인 구현

장식자 패턴 구현을 위한 클래스를 믹스인(Mixins) 클래스로 정의하겠습니다. 믹스인은 특정 기능(행위)만을 담당하는 클래스로, 단독 사용(Stand-alone use)이 아닌 다른 클래스에 탑재되어 사용될 목적으로 작성된 (조각) 클래스를 의미합니다. 믹스인에 대한 자세한 내용은 자바스크립트 Class 와 편의문법, 그리고 믹스인(Mixins) 기법을 참고해주세요.

아래 Decoratable 믹스인 클래스는 기존 메서드의 연산 결과물을 동적으로 장식할 수 있는 역할을 담당하는 decorate 메서드를 제공합니다.

// Decoratable 믹스인 클래스
const Decoratable = superclass => class extends superclass {
	decorate(referenceName, decorator){
    	// reference 는 기존 메서드를 참조
        const reference = this[referenceName];
        
        // 오버라이딩을 통해 장식자 구현
        this[referenceName] = (...args) => {
        	// reference 메서드 연산수행 후, 결과를 첫번째 인자로 담아 decorator 호출
        	return decorator.apply(this, [
            	reference.call(this, ...args),
                ...args
            ])
        }
    }
}
  • decorate 메서드는 기존 메서드 식별을 위한 referenceName 과 이를 장식하는 데코레이팅 함수 decorator 를 인자로 취합니다.

  • reference 는, this 인스턴스에서 referenceName 과 매칭되는 기존 메서드에 대한 참조를 담고있습니다.

  • 이제 기존 메서드를 오버라이딩하는 내부 구현부에 reference 함수의 실행 결과는 decorator 함수가 실행될 때 첫번째 인자값으로 전달됩니다.

설명만으로는 감이 안오죠? Decoratable 믹스인 클래스를 실제 상속구조에 넣고 실행하는 예제를 봅시다.

// super class
class PureElement {
	constructor(name){
    	this.name = name;
    }
    
    element(){
    	return document.createElement('div');
    }
}

// BorderedElement 는 PureElement 와 Decoratable 을 상속.
class BorderedElement extends Decoratable(PureElement) {
	constructor(name){
    	super(name);
        
        // This is private
        const appendBorder = (element) => {
        	element.style.border = '1px solid';
            return element;
        }
        
        // 실제 decorating 수행
        this.decorate('element', (decoObj) => {
        	return appendBorder(decoObj);
        });
    }
}

new BorderedElement('borderDiv').element();

// [output] ==> <div style="border:1px solid"></div>

BorderElement 는 PureElement에서 유래되어, Decoratable 믹스인 클래스를 통해 decorate 메서드를 상속받습니다.

BorderElement 의 생성자(constructor)안에서 상속받은 믹스인 클래스의 decorate 메서드가 사용됩니다. decorate 메서드는 element 메서드에 대한 장식을 수행하고 그 결과를 반환(return)합니다. 이를 통해 추후(런타임도 포함)에 다시한번 장식이 필요할 때 현재의 결과를 전달받을 수 있습니다.

BorderElement 생성자안에 선언된 appendBorder 함수에 주목해주세요. 자바스크립트 클래스문법에서 private 을 선언할 수 있는 유일한 공간은 객체 생성자 내부뿐입니다. 이때 public 메서드인 element 는 생성자 내부에 선언된 함수에 접근할 수 없지만, 장식자 기법이 이를 가능케합니다.

마지막으로 BorderElement 의 인스턴스의 element 메서드 반환값은 border 가 더해진 div 앨리먼트입니다.

장식자 패턴에서 이전 연산의 수행결과가 필요한 이유

데코레이팅은 본래 기존 연산에 덫붙이는 작업이 필요할 때 사용하는 디자인 패턴입니다. 물론 기존 결과물에 상관없이 필요한 행위가 연달아 호출되는 구조로 사용될 수도 있지만, 결과물의 반환은 이 후에 장식이 필요한 시점에 최종 장식 결과물을 전달해줍니다. 아래 예제는 코드 작성자가 최종 결과물을 전달받아 element 메서드를 다시한번 장식하는 과정을 보여줍니다. (decorate 는 public 메서드이기에 가능합니다.)

let el = new BorderedElement('borderedDiv');
el.decorate('element', (decoObj) => {
	decoObj.style.background = '#eee';
    return decoObj;
});

el.element();
// [output] ==> <div style="border:1px solid #eee"></div>

맺음말

아래에 몇가지 고려해볼 말한 추가사항이있습니다.
1. decorate 메서드의 전달인자가 object literal({}) 방식이면, for-in-loop 방식으로 기술된 N개 이상의 메서드를 장식할 수 있기에 더 유연한 구조를 갖을 수 있습니다.

  1. 장식 대상인 기존 reference 메서드가 결과를 반환하지 않을 때, 분명한 경고 메세지를 사용자에게 보여주는 것도 좋은 정책입니다.
profile
코딩꾼

2개의 댓글

comment-user-thumbnail
2019년 12월 8일

좋은 글 감사합니다. 다음 시리즈도 기대되네요.

답글 달기
comment-user-thumbnail
2021년 6월 26일

데코레이터 문법이 아니라 데코레이터 패턴(!) 이네요ㅎㅎㅎ
햇깔릴뻔....
찾는 정보는 아니였지만 좋은 글 감사합니다~

답글 달기