decorator
NestJs로 서버를 개발하다보면 무조건 접할 수 밖에 없는 개념이 바로 "데코레이터"이다. 이 녀석을 잘 활용하면 횡단 관심사를 분리하여 관점 지향 프로그래밍을 적용한 코드를 작성할 수 있게 된다. 클래스, 메서드, 접근자, 프로퍼티, 매개 변수에 적용할 수 있으며 각 요소의 선언부 앞에 @로 시작하는 데코레이터를 선언하게 된다. 이를 통해 런타임 때 데코레이터로 구현된 코드를 함께 실행하는 것이다.
이 포스트를 작성하는 시점에서 decorator는 아직 JS 표준이 아니며 TS에서 아래의 두 옵션을 설정해주어야 한다.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
Decorator Factory
만약 데코레이터에 인자를 넘겨서 그 동작을 외부에서 변경할 수 있도록 만들고 싶다면, 데코레이터 함수를 리턴하는 함수, 즉 데코레이터 팩토리를 사용하면 된다.
const foo = (value: string) =>
(target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
console.log("value : " + value)
console.log("target : " + target)
console.log("propertyKey : " + propertyKey)
}
class Test {
@foo("test")
testMethod() {}
}
const t = new Test()
t.testMethod() // "value : test", "target : Person.prototype", "propertyKey : testMethod"
하나의 요소에 여러 데코레이터를 동시에 달아줄 수 있는데, 이를 데코레이터 합성이라 부른다. 각 데코레이터는 순서대로 스택에 쌓이게 된다. 만약 데코레이터 팩토리를 사용한다면, 우선 데코레이터 팩토리가 스택에서 해결되고, 이후 리턴된 데코레이터가 스택에 쌓인다. 일반적으로는 위에서 아래로 평가되고, 아래에서 위로 실행된다는 표현을 더 많이 사용하는 듯하다.
Class Decorator
클래스 데코레이터는 클래스 자체, 즉 생성자 함수를 인자로 받는다. 이를 통해 장식된 클래스의 생성자 함수를 확장하거나, 생성자 함수의 프로토타입을 이용하여 확장할 수 있다. 클래스 데코레이터는 수정된 생성자 함수를 리턴하거나, 수정하지 않고 그대로 리턴할 수도 있다.
나는 생성자 함수를 확장하여 특정한 프로퍼티를 추가하거나, 프로토타입에 새로운 메서드를 추가하는 식으로 사용하고 있다.
const addMethod = <T extends { new (...args: any[]): {} }>(constructor: T) {
constructor.prototype.newMethod = function() {
console.log('New method added!');
};
}
@addMethod
class Test {
constructor(name) {
this.name = name;
}
}
const obj = new MyClass('John');
obj.newMethod(); // New method added!
Method Decorator
메서드 데코레이터는 클래스의 메서드에 적용되어 해당 메서드의 동작을 변경하거나 추가적인 기능을 부여할 수 있다. 데코레이터 팩토리에서 잠깐 살펴보았던 것처럼, 메소드 데코레이터는 target, name, descriptor라는 인자를 전달받게 된다. 각각의 인자는 아래와 같으며, 메소드 데코레이터의 리턴 값은 무시된다.
- target: static 메서드라면 클래스의 생성자 함수, 인스턴스의 메서드라면 클래스의 prototype 객체
- propertyKey : 메서드 이름
- descriptor : 해당 속성이나 메서드의 동작을 정의한다(PropertyDescriptor라는 타입을 받음)
메서드 데코레이터는 기본적으로 함수의 속성 기술자를 수정한다. 이는 descriptor.value를 재정의하여 새로운 기능을 추가하거나, 기존 기능을 확장하는 방식으로 이루어진다. 아래의 예시처럼 주로 메서드 호출을 로그에 남기거나, 결과를 캐시하는 메소드 데코레이터를 만들어 많이 사용한다.
const log = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${propertyKey} with arguments: ${args}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
const cache = (target: any, propertyKey: string, descriptor: PropertyDescriptorr) => {
const originalMethod = descriptor.value;
const cacheMap = new Map();
descriptor.value = function(...args) {
const key = JSON.stringify(args);
if (!cacheMap.has(key)) {
const result = originalMethod.apply(this, args);
cacheMap.set(key, result);
}
return cacheMap.get(key);
};
return descriptor;
}
class Calculator {
@log
add(a, b) { return a + b; }
@log
subtract(a, b) { return a - b; }
@cache
factorial(n) {
if (n <= 1) return 1;
return n * this.factorial(n - 1);
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Calling add with arguments: 2,3
console.log(calculator.factorial(5)); // 120
console.log(calculator.factorial(5)); // 캐시된 결과: 120
Property Decorator
메소드 데코레이터와 달리 descriptor 인자를 받지 않으며, 대신 property descriptor 형태의 객체를 리턴함으로써 프로퍼티의 동작이나 특성을 변경하는 데 사용된다. 주로 어떤 프로퍼티를 읽기 전용(writable: false)으로 만들거나, 프로퍼티에 대한 접근자를 추가하는 데 사용하고 있다.
// const readonly = (target, propertyKey) => {
// return {
// writable: false
// });
// }
const readonly = (target, propertyKey) => {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
const logAccess = (target, propertyKey) => {
let value = target[propertyKey];
Object.defineProperty(target, propertyKey, {
get() {
console.log(`Getting value of ${propertyKey}: ${value}`);
return value;
},
set(newValue) {
console.log(`Setting value of ${propertyKey} to ${newValue}`);
value = newValue;
},
enumerable: true,
configurable: true
});
}
Parameter Decorator
매개변수 데코레이터는 클래스 메서드의 개별 매개변수에 적용되어 그 매개변수의 메타데이터를 설정하거나, 매개변수의 유효성 검사와 같은 추가적인 로직을 적용할 수 있다. descriptor 대신 데코레이터가 적용된 매개변수의 인덱스 값을 나타내는 parameterIndex 인자를 받는다.
const validate = (Fn: (arg: any) => boolean) =>
(target, propertyKey, parameterIndex) => {
const originalMethod = target[propertyKey];
target[propertyKey] = function (...args) {
if (!Fn(args[parameterIndex])) {
throw new Error(`Invalid parameter at index ${parameterIndex} in method ${propertyKey}: expected a number`);
}
return originalMethod.apply(this, args);
};
}
const isNumber = (arg: unknown) => {
typeof arg == "number"
}
class Calculator {
multiply(@validateNumber(isNumber) a, b) {
return a * b;
}
}
블로그의 정보
Ayden's journal
Beard Weard Ayden