Decorator
란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 디자인 패턴을 말합니다(위키백과). 코드를 수정하지 않고도 수평적으로 기능을 확장하거나 동작을 추가할 수 있고, 중복 코드도 줄일 수 있는 유용한 방법입니다. 프레임워크 등을 통해 유사한 기능을 사용/구현해본 적이 있다면 익숙하게 와닿는 개념일 수 있습니다.
자바스크립트에서도 사용할 수는 있지만 글 작성 시점 기준 현재 아직 표준은 아니며, 표준 제정을 위한 ES proposal stage 3 까지 올라온 상황입니다. 만약 본격적으로 사용하고자 한다면 babel과 같이 트랜스파일러의 도움이 필요할 수 있습니다.
반면 자바스크립트 대비 더 과감하고 적극적인 신규 문법 도입이 가능한 타입스크립트 진영에서는 이미 적극적으로 도입하여 사용하고 있었기도 한데, 대표적인 예로 Nest.js나 TypeORM 같은 백엔드 영역을 들 수 있겠습니다.
아래와 같이 함수 호출시 다른 함수를 래핑하는 방법으로 Decorator
를 구현할 수 있기도 합니다. 어디까지나 '패턴'에 관한 토픽이기 때문입니다. 기본적으로 새로운 함수를 반환하여 전달된 함수의 동작을 수정하는 방식이라고 볼 수 있습니다.
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
과 같은 형태로 적용 대상의 바로 위에 추가하여 사용합니다. 현재 제안/설계된 스펙에 따르면, 아래와 같은 대상에만 적용할 수 있으며 추후 후속 작업을 통해 확장될 가능성이 있습니다.
클래스의 필드를 읽기 전용으로 만드는 예제 코드를 인용해 보겠습니다.
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
라는 정보가 존재합니다. 위 예제에서는 이것을 변경한 것으로, 각 속성이 의미하는 바는 아래와 같습니다. 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 커넥션 등 상황에 따라 여러 형태로 유용하게 사용할 수 있을 것입니다.