데코레이터(Decorator
)란, 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 디자인 패턴을 말합니다. 코드를 수정하지 않고도 수평적으로 기능을 확장하거나 동작을 추가/수정할 수 있고, 중복 코드도 줄이는 등의 목적으로도 유용하게 활용할 수 있는 수단이 됩니다.
'래퍼(Wrapper
)'라고도 지칭되는데, 마치 어떤 옷을 껴입는 것처럼 어떤 객체를 '감싸' 집합 관계를 구현한다는 점에서 아주 적절한 명칭이기도 합니다.
자바스크립트에서도 이러한 개념이 존재하며 실제로 구현할 수도 있지만(함수를 일급 객체로 취급하는 모든 프로그래밍 언어는 데코레이터를 구현할 수 있습니다), 글 작성 시점 기준 현재 아직 표준 스펙은 아니며, 표준 제정을 위한 ES proposal stage 3 까지 올라온 상황입니다. 그래서 만약 당장 실무에서 본격적으로 사용하고자 한다면 babel과 같이 트랜스파일러의 도움을 받아야 할 수 있습니다.
반면 자바스크립트 대비 조금 더 과감하고 적극적인 신규 문법 도입이 가능한 타입스크립트 진영에서는, 마찬가지로 실험적인 기능이긴 하지만 이전부터 도입하여 사용하고 있었기도 한데, 대표적으로는 Nest.js
나 TypeORM
같은 백엔드 생태계를 예로 들 수 있겠습니다.
간단한 예를 하나 들어보자면, 아래와 같이 함수 호출시 다른 함수를 래핑하는 방법으로 데코레이터를 구현해볼 수 있기도 합니다. 새로운 함수를 반환하여 전달된 함수의 동작을 수정하는 방식이라고 볼 수 있습니다.
function doSomething(message) {
console.log(`${message}`);
}
function loggingDecorator(wrapper) {
return function() {
console.log('logging start');
const result = wrapper.apply(this, arguments);
console.log('logging finished');
return result;
}
}
const wrapper = loggingDecorator(doSomething);
wrapper('Hello!');
// 'logging start'
// 'Hello!'
// 'logging finished'
Python
과 유사하게, @expression
과 같은 형식으로 적용할 대상의 바로 위에 추가하여 선언할 수 있습니다. 현재까지 제안/설계된 스펙에 따르면, 아래와 같은 대상에 대해서만 적용할 수 있으며, 추후 후속 작업을 통해 점차 확장될 가능성이 있습니다.
클래스의 필드를 읽기 전용(readonly)으로 만드는 예제 코드를 인용해 보겠습니다.
function readonly(target, key, descriptor) {
return {
...descriptor,
writable: false,
};
}
class Data {
@readonly
password = 'pwd';
}
const data = new Data();
data.password = 'newpwd'; // Error: Cannot assign to read only property 'password' of object '#<Data>'
데코레이터 함수는 인자로 target
, key
, descriptor
를 전달받고 있습니다. target
은 데코레이팅 대상, key
는 대상의 속성 이름, descriptor
는 데코레이팅되는 속성을 포함한 대상의 모든 속성들을 의미합니다. 여기서 주목해 보아야 할 부분은 설명자 객체, descriptor
입니다.
const person = {
name: 'sj',
level: 99
};
console.log(Object.getOwnPropertyDescriptor(person, 'level'));
/*
{
configurable: true
enumerable: true
value: 99
writable: true
}
*/
자바스크립트 객체의 프로퍼티에는 속성 설명자, propertyDescriptor
라는 특별한 정보가 존재합니다. 겉으로 드러나지 않고 숨겨져 있기 때문에 평소에는 간과하기 쉽지만, 데코레이터에 대해 이해할 때는 이를 인지하고 있어야 합니다.
위 예제는 속성 설명자를 핸들링할 수 있는 Object.getOwnPropertyDescriptor
를 사용하여 변경하는 것을 보여주고 있는데, 이때 각 속성이 의미하는 바는 아래와 같습니다. MDN 문서
계속해서 클래스 메소드에 데코레이터를 적용하는 예시를 인용해 보겠습니다. 위에서 함수를 래핑하여 명령형으로 데코레이터를 구현한 방법과 유사한 형태입니다.
function logging(target, name, descriptor) {
const originalValue = descriptor.value;
const wrapper = function(...arguments) {
try {
return originalValue.apply(this, arguments);
} catch (error) {
console.error(`Error: ${error}`);
throw error;
}
};
return { ...descriptor, wrapper };
}
class Data {
@logging
getData = () => {
throw new Error('no data');
};
}
const data = new Data();
data.getData(); // 'Error: no data'
이미 데코레이터를 프로덕션 코드에서 사용하고 있는 케이스가 존재할 것이지만, 아직 현재 시점에서도 제안 단계인지라 추후 명세는 변경될 수 있는 상황입니다. 추후 호환성을 깨뜨릴 수 있는 변경이 발생할 수 있고, 그렇게 되면 추가적인 조치나 트랜스파일러의 지원에 더 의존해야 하게 될 수도 있습니다.
다만 그렇더라도 구체적인 구현 방법, 코드가 변화할 뿐 기본적인 의도나 패턴 자체가 변화되는 것은 아니므로, 어느 정도 최신 기능을 사용하는 데에 대한 약간의 비용을 감내한다는 스탠스로 접근한다면 얼마든지 실무에서도 적용해 볼 수 있는 부분이 아닐까 합니다.
반복되는 공통 코드를 다른 코드에 횡단하여 간단하고 편리한 방법으로 적용할 수 있다는 점은 매력적입니다. 위의 예제에서처럼 로깅, 에러 핸들링, 유효성 검사, DB 커넥션 등 상황에 따라 여러 형태로 유용하게 사용할 수 있을 것입니다.