javascript의 Decorator
최근 node로 개발하면서 직면한 여러 문제들을 해결하기 위해서 typescript를 공부하는 중 decorator에 대해서 관심을 가지게 됐다. decorator는 typescript의 spec은 아니고 ECMAScript 표준의 proposal중 하나로 (proposal link) experimentalDecorators option을 통해 typescript에서 사용할 수 있다. javascript에서도 babel을 통해 사용할 수 있다. 물론 proposal 이기 때문에 정식 표준이 안될수도 있다는 점은 감안해야 한다.
decorator vs java annotation
먼저 코드부터 보고 시작하는 편이 나을거 같다. 아래는 decorator를 적용한 class의 모습이다.
@Greeter
class Person {
constructor(name) {
this._name = name;
}
@Repeat(3)
sayHello() {
console.log(`Hello I'm ${this._name}`);
}
}
java 개발자들은 이 코드를 보고 나면 "이거 annotation 아니야?"라고 생각할 것이다. 문법적으로 봤을때 java의 annotation과 큰 차이는 없다. java의 annotation과 마찬가지로 class, method, class field에 decorator를 적용할 수 있다(class field는 아직 proposal 상태다 proposal link). 그리고 여러개의 decorator를 중첩해서 쓸 수 있다. 차이점이 있다면 decorator는 runtime에서만 역할을 한다는 점이다. java의 annotation은 Retention이라는게 있어 compiler에게만 보이고 runtime에는 없어지게 하거나 runtime에도 살아남아서 jvm에 의해서 참조 할 수 있게 할 수도 있다. javscript는 정적인 타입이 없기 때문에 애초에 annotation 처럼 compile time에 기능을 할 수는 없다(이 부분은 typescript를 도입해도 마찬가지). 이런 측면에서 runtime에서만 활용될 수 있는 annotation이라고 볼 수도 있다. 그렇다면 decorator는 어떻게 정의하는가? 아래는 decorator를 선언하는 코드다.
function Repeat(count) {
return function(target, propertyKey, descriptor) {
const targetMethod = target[propertyKey];
descriptor.value = function(...args) {
const boundMethod = targetMethod.bind(this);
for(let i = 0; i < count; i++) {
boundMethod(...args);
}
};
return descriptor;
};
}
- target : 해당 field를 가지고 있는 Class의 prototype
- propertyKey : 해당 field의 이름
- descriptor : method의 property descriptor (property descriptor에 대한 내용은 여기참고)
java runtime에서 annotation을 활용하려면 보통 별도의 annotation processor를 개발해서 사용하지만, decorator는 선언하고 class나 field에 적용하면서 바로 호출되기 때문에 사용하는 방식이 약간 차이가 있다.
Decorator는 어떻게 활용하는가?
javascript에서 decorator를 활용하는 방법은 크게 두가지가 있는거 같다.
- class 또는 field 검증 - 선언된 class가 적합한지 검증하는 방식이다. java에서
@override
가 하는 역할과 같다. 비록 javascript는 compile time에 이런 error를 잡을 수 없지만 runtime에 class 선언과 동시에 fail fast 할 수는 있다. 위에서 제시한@Greeter
라는 decorator로 예를 들어 보자.@Greeter
가 달려있는 class는 say로 시작하는 method가 하나 이상 있어야 한다고 하자. 그럼 다음과 같은 decorator를 만들 수 있다.function Greeter(target) { const propertyNames = Object.getOwnPropertyNames(target.prototype); if (!propertyNames.find(propertyName => propertyName.indexOf('say') === 0)) { throw new Error(`Greeter class should have method which starts with 'say'.`); } return target; }
- class 또는 field 변경 - 위 예시에서
@Repeat
같은 decorator를 예로 들 수 있다. decorator는 class 객체 자체를 받기 때문에 어떤 형태로든 class를 변형할 수 있어서 활용할 방법이 무수히 많다.
나는 Decorator로 어떤 문제를 해결했나?
실제 개발 환경에서 유용하게 사용 될 수 있는 decorator는 core-decorators에 이미 개발되어 있다(github). 개인적으로는 core-decorators의 @autobind
를 가장 적극적으로 활용한다. 많은 개발자들이 javascript에서 this의 활용에 관해서 불편함을 느끼고 있다. 특히 class의 method를 다른 함수의 인자로 넘겨주면 문제가 발생하는 경우가 많다. 그래서 아래와 같이 해결하는 경우가 많다.
class Person {
constructor(name) {
this._name = name;
this.sayHello = this.sayHello.bind(this);
}
sayHello() {
console.log(`Hello I'm ${this._name}`);
}
}
이렇게 개발하면 항상 의도대로 this를 사용할 수 있다. 하지만 method를 새롭게 선언할때마다 constructor에서 bind해줘야 하기 때문에 유지보수성 측면에서 매우 나쁘다. 이 문제는 core-decorator의 @autobind
를 사용하면 손쉽게 해결할 수 있다.
import {autobind} from 'core-decorators';
@autobind
class Person {
constructor(name) {
this._name = name;
}
sayHello() {
console.log(`Hello I'm ${this._name}`);
}
}
const person = new Person('Ikanny');
const sayHello = person.sayHello;
sayHello(); // => print 'Hello I'm Ikanny'
이렇게 해도 this를 사용하는 모든 class에 decorator를 붙여야 하기 때문에 모든 걱정을 내려놓을 수는 없다. 그래도 기존에 존재하던 해결방법보다는 한단계 더 세련되어 보인다. core-decorators에는 이 밖에도 좋은 decorator가 많지만 여기서 다 소개하지는 않겠다.
마무리
요즘 java를 개발하면 굉장히 많은 annotation을 쓴다. spring, lombok, jpa... 등등 java에서 자주 사용되는 framework과 library가 자체적인 annotation을 제공하는 경우가 많다. 그래서 어떤 class는 정말 한눈에 다 안들어올 정도로 많은 annotation을 달고 있는 경우가 많다. 그리고 소스코드가 커지다 보면 어느 순간 어떤 annotation이 어떤일을 하는지 가늠이 잘 오지 않는 지경에 이르기도 한다. 개인적으로 이런 부분은 java개발 할 때 좋아하지 않았기 때문에 javascript에서도 웬만해선 decorator를 활용해서 문제를 풀어나가려 하지는 않는다. 개인적으로 @override
같은 decorator까지 꼭 써야 할까? 라는 생각은 든다. 다만 decorator를 사용할때 쉽게 풀리는 문제나 코드의 유지보수성을 높여줄 수 있는 상황에는 적극적으로 사용할 생각이다. 이후 decorator를 활용한 좋은 패턴을 알게 되면 다시 post를 올려야겠다.