Decorator 소개
TypeScript 및 ES6에는 Class를 도입했습니다. 클래스와 클래스 멤버를 사용하게 되면 필연적으로 Decorator를 찾게 됩니다. 가장 큰 이유는 Java, C#, 파이썬과 같은 언어에서 Decorator를 지원하기 때문입니다.
심지어 Spring Framework와 같은 거대 프레임워크를 사용한 경험이 있다면 Decorator의 코드 재사용성은 매우 훌륭하다고 할 수 있습니다. ES6의 Javascript에 도입 되었으며, TypeScript도 지원하고 있습니다.
Decorator는 일종의 함수로 코드 조각을 장식해주는 역할을 합니다. 메소드, 클래스, 프로퍼티, 파라미터 등 위에 @(골뱅이)로 시작하는 데코레이터를 선언하고, 런타임에 데코레이터 함수를 실행하여, 추가적인 기능을 제공합니다.
데코레이터를 활용하면 횡단 관심사(cross-cutting concern)를 분리하여 관점 지향 프로그래밍을 적용할 수 있습니다. 또한, 데코레이터 패턴은 클래스를 수정하지 않고, 클래스의 멤버 정의를 수정 및 기능을 확장할 수 있는 구조적 패턴입니다. 이 패턴은 특정 인스턴스에 초점을 맞춰서 기능 개발이 가능해집니다.
💡 횡단 관심사(Cross-cutting concern)는 핵심 관심에 영향을 주는 프로그램의 영역으로, 로깅과 트랜잭션, 인증처리와 같은 시스템 공통 처리 영역이 해당된다.
Decorator 특징
- 데코레이터는 클래스 선언, 메서드(method), 접근자, 프로퍼티(property), 매개 변수(parameter)에 첨부할 수 있는 특수한 종류의 선언입니다.
- 데코레이터 함수에는 target, key, descriptor가 전달됩니다.
- 메소드나 클래스 인스턴스가 만들어지는 런타임에 실행됩니다. 매번 실행되는 것이 아닙니다.
- 데코레이터는 위에서 말한 속성만 장식할 수 있습니다.
데코레이터 설정
데코레이터를 사용하기 위해서는 command를 입력하거나 tsconfig.json에서 설정해야 합니다.
tsc --target ES5 --experimentalDecorators
위는 command 명령어 입니다.
//tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators"; true
}
}
위 코드는 tsconfig.json에 작성한 옵션 코드입니다.
이렇게 작성하면 데코레이터을 사용할 수 있습니다.
데코레이터 작성
이번에는 데코레이터를 어떻게 작성하고 활용하는지 살펴보겠습니다.
어떻게 작성해야 데코레이터를 호출하여 사용할 수 있을까요? 예제를 보죠.
function exampleDecorator(target) {
console.log("Decorator example");
}
class Test {
@exampleDecorator()
test() {
console.log("Test class에 test method 입니다.");
}
}
const t = new Test();
t.test();
Decorator example
Test class에 test method 입니다.
위와 같이 동작하는 것을 볼 수 있습니다. 어렵지 않게 데코레이터를 작성할 수 있죠.
데코레이터 팩토리(Decorator Factories)
데코레이터 팩토리는 데코레이터를 선언하는 방식을 바꾸고 싶을 때 사용하는 래퍼 함수입니다. 앞서 사용한 코드처럼 선언하여 사용할 수도 있지만, 좀 더 디테일한 조건을 구현하고자 할 때 사용합니다.
사용자로부터 전달 받은 인자를 활용하여 내부에서 반환되는 데코레이터 함수에 사용할 수 있도록 합니다. 아래 예시 코드를 살펴보조.
function factory(value: string) {
console.log('데코레이터 팩토리');
return function () {
console.log(value);
};
}
class Test {
@factory('Hi')
test() {
console.log('test 함수 입니다.');
}
}
const t = new Test();
t.test();
데코레이터 팩토리
Hi
test 함수수입니다.
위와 같이 활용할 수 있습니다. 조건식을 넣어서 한다면 들어오는 value 값에 따라 반환되는 데코레이터 함수도 지정할 수 있습니다.
데코레이터 합성(Decorator Composition)
데코레이터 합성은 선언에 여러 데코레이터를 적용할 수 있는 특징입니다.
여러 데코레이터가 단일 선언에 적용되는 경우 수학에서 합성 함수와 유사하게 적용됩니다.
즉, f(g(x)) 와 같은 방식으로 적용됩니다.
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
위와 같이 사용이 가능하며 출력은 아래와 같이 출력됩니다.
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
위 예제를 보면 각 데코레이터 표현식은 위에서 아래 방향으로 평가되는 걸 알 수 잇습니다.
그리고 실행 결과는 아래에서 위로 함수를 호출하는 것도 알 수 있습니다.
두 번째 줄까지는 평가, 3,4번째 줄은 함수 호출입니다.
데코레이터 종류
Class Decorator
클래스 데코레이터는 클래스 선언 직전에 선언됩니다. 클래스 데코레이터는 클래스 생성자에 적용되며 클래스 정의를 관찰, 수정 또는 교체하는 데 사용할 수 있습니다. 또한, 선언 파일이나 다른 주변 컨텍스트 (예: 선언
클래스)에서 사용할 수 없습니다.
클래스 데코레이터의 표현식은 데코레이팅된 클래스의 생성자를 유일한 인수로 런타임에 함수로 호출됩니다. 함수가 값을 반환하면 클래스가 선언을 제공하는 생성자 함수로 바꿉니다.
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
위와 같이 클래스에서 사용되는 데코레이터를 클래스 데코레이터라고 합니다.
결론적으로 클래스 데코레이터는 클래스의 생성자 클래스 정의를 읽거나 수정할 수 있기에 기존 클래스 정의를 확장하는 용도로 사용합니다.
// 데코레이터 팩토리
function classDecorator(param1: string, param2: string) {
// 데코레이터 함수
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
new_prop = param1;
first_prop = param2;
};
};
}
@classDecorator('안녕하세요', '반갑습니다')
class Test {
first_prop: string;
constructor(m: string) {
this.first_prop = m;
}
}
let t = new Test('world');
console.log(t);
console.log(t.first_prop);
위 코드는 클래스 데코레이터 팩토리 사용 예제 입니다.
실행되면 아래와 같이 나옵니다.
Test {
first_prop: '반갑습니다',
new_prop: '안녕하세요'
}
반갑습니다.
생성자에 world를 넣었지만 classDecorator가 최종적으로 반영되면서 데코레이터에 들어간 paramater 값이 적용됩니다.
메서드 데코레이터
메서드 데코레이터는 메서드 선언 직전에 선언됩니다. 데코레이터는 메서드의 **프로퍼티 설명자(Property Descriptor) ****에 적용되며 메서드 정의를 관찰, 수정 또는 대체하는 데 사용할 수 있습니다. 메서드 데코레이터는 선언 파일, 오버로드 또는 기타 주변 컨텍스트(예: 선언
클래스)에서 사용할 수 없습니다.
메서드 데코레이터의 표현식은 런타임에 다음 세 개의 인수와 함께 함수로 호출됩니다:
- 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입 입니다.
- 멤버의 이름
- 멤버의 프로퍼티 설명자
메서드 데코레이터가 값을 반환하면, 메서드의 프로퍼티 설명자 로 사용됩니다.
아래 코드는 메서드에 적용된 메서드 데코레이터입니다.
function methodDecorator() {
return function (target: any, property: string, descriptor: PropertyDescriptor) {
// descriptor.value는 test() 함수 자체를 가리킨다. 이 함수를 잠시 변수에 피신 시킨다.
let originMethod = descriptor.value;
// 그리고 기존의 test() 함수의 내용을 다음과 같이 바꾼다.
descriptor.value = function (...args: any) {
console.log('before');
originMethod.apply(this, args); // 위에서 변수에 피신한 함수를 call, apply, bind 를 통해 호출
console.log('after');
};
};
}
class Test {
property = 'property';
hello: string;
constructor(m: string) {
this.hello = m;
}
@methodDecorator()
test() {
console.log('test');
}
}
let test = new Test("world")
test.test()
[LOG]: "before"
[LOG]: "test"
[LOG]: "after"
위와 같이 출력이 됩니다.
이 데코레이터는 활용에 따라 메서드 실행 전/후 로깅이나 실행시간 등 다양한 횡단 관심사를 추가할 수 있습니다.
접근자 데코레이터
접근자 데코레이터는 접근자 선언 바로 전에 선언됩니다. 접근자 데코레이터는 접근자의 프로퍼티 설명자에 적용되며 접근자의 정의를 관찰, 수정 또는 교체하는 데 사용할 수 있습니다. 접근자 데코레이터는 선언 파일이나 다른 주변 컨텍스트(예: 선언
클래스)에서 사용할 수 없습니다.
💡 접근자는 우리가 흔히 사용하는 getter, setter 등을 의미합니다. 타입스크립트에서는 get, set 키워드가 있습니다.
접근자 데코레이터의 표현 식은 세 가지 인수를 전달 받습니다.
- static 프로퍼티가 속한 클래스의 생성자 함수 또는 인스턴스 프로퍼티에 대한 클래스의 프로토 객체
- 해당 메서드의 이름
- 해당 메서드의 descriptor
이에 대한 반환 값은 아래와 같습니다.
- Property Descriptor
- void
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
const p = new Point(10, 20);
delete p.x; // The operand of a 'delete' operator cannot be a read-only property.
console.log(p.x);
위 코드는 typescript playground에서는 제대로 실행이 됩니다.
다만 주석과 같은 오류를 출력하게 됩니다.
이러한 속성을 통해 다른 개발자가 사용하는 접근자를 제어할 때 미리 경고를 줄 수 있는 방식으로도 활용이 가능합니다.
프로퍼티 데코레이터
속성 데코레이터는 클래스의 프로퍼티 선언 전에 선언됩니다.
특징은 Property Descriptor가 인자로 제공되지 않습니다. 대신 Property Descriptor 객체를 반환함으로써 프로퍼티 설정을 바꿀 수 있습니다.
프로퍼티 데코레이터에는 두 개의 인수가 존재합니다.
- 정적 멤버에 대한 클래스 생성자 함수나 인스턴스 멤버에 대한 클래스 프로토타입
- 멤버의 이름
아래는 프로퍼티 데코레이터 예시 코드입니다.
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
const g = new Greeter('Greeter is good!');
console.log(g.greet());
[LOG]: "Hello, Greeter is good!"
프로퍼티 데코레이터는 위와 같이 속성 값에 선언할 수 있으며, 속성 값을 특정하거나 원하는 방식대로 변경하고자 할 때 사용됩니다.
추가 예제 코드를 보죠.
function SetDefaultValue(numberA: number, numberB: number) {
return (target: any, propertyKey: string) => {
const addNumber = numberA * numberB;
let value = 0;
// 데코레이터가 장식된 DataDefaultType의 num 이라는 프로퍼티의 객체 getter / setter 설정을 추가한다.
Object.defineProperty(target, propertyKey, {
get() {
return value + addNumber; // 조회 할때는 더하기 시킴
},
set(newValue: any) {
value = newValue - 30; // 설정 할때는 30을 뺌
},
});
};
}
class DataDefaultType {
@SetDefaultValue(10, 20)
num: number = 0;
}
const test = new DataDefaultType();
test.num = 30;
console.log(`num is 30, 결과 : ${test.num}`);
test.num = 130;
console.log(`num is 130, 결과 : ${test.num}`);
test.num = 20;
console.log(test.num);
[LOG]: "num is 30, 결과 : 200"
[LOG]: "num is 130, 결과 : 300"
[LOG]: 190
위 코드는 SetDefaultValue를 데코레이터로 생성한 코드입니다. 이 코드를 보니 실제 Product에서 어떻게 활용할 것인지 눈에 보입니다. 변하지 않는 계산식을 넣거나 처리할 때 활용하기 좋아보입니다.
매개변수(Parameter) 데코레이터
파라미터에 직접적으로 선언하는 데코레이터로써 클래스 생성자 또는 메서드 선언 함수에 적용됩니다.
사용 되는 인수는 세 개 입니다.
- 클래스의 생성자 함수 또는 프로퍼티 객체
- 메서드의 이름
- 매개변수 순서 인덱스
아래는 매개변수 데코레이터 예시 코드 입니다.
// 매개 변수 데코레이터 함수
// target 클래스의 validators 속성에 유효성을 검사하는 함수 할당
function MinLength(min: number) {
return function(target: any, propertyKey: string, parameterIndex: number) {
target.validators = {
minLength: function(args: string[]) {
return args[parameterIndex].length >= min;
}
}
}
}
// 메서드 데코레이터 함수
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
// 설명자의 value에 유효성 검사 로직이 추가된 함수를 할당
descriptor.value = function(...args) {
// target에 저장해둔 validators를 모두 수행한다. 이때 원래 메서드에 전달된 인수들을 각 validator에 전달한다.
Object.keys(target.validators).forEach(key => {
if (!target.validators[key](args)) {
throw new Error();
}
})
// 기존 함수를 실행
method.apply(this, args);
}
}
class User {
private name: string;
@Validate
setName(@MinLength(3) name: string) {
this.name = name;
}
getName() {
return this.name;
}
}
const t = new User();
t.setName('hello');
console.log(t.getName());
console.log('---------------');
t.setName('hi');
console.log(t.getName());
console.log('---------------');
[LOG]: "hello"
[LOG]: "---------------"
[ERR]: "Executed JavaScript Failed:"
[ERR]: Error: {}
여기서 작성된 데코레이터는 2개 입니다. 하나는 파라미터 데코레이터, 하나는 메서드 데코레이터 입니다.
파라미터 데코레이터는 전달된 문자열 길이가 3보다 작은지 확인해서 결과를 반환합니다.
그 다음 메서드 데코레이터에서 결과를 체크 후 에러를 반환하도록 작성된 코드입니다.
그렇기 때문에 길이가 3보다 큰 hello는 제대로 동작하며, 3보다 짧은 hi 문자열은 에러를 반환하게 됩니다.
이를 통해 파라미터 데코레이터는 각 파라미터를 체크하는 검증 함수를 작성하는데 용이하다는 것을 알 수 있습니다.
데코레이터 정리
데코레이터 | 역할 | 호출 시 전달되는 인수 | 선언 불가능한 위치 |
---|---|---|---|
클래스 데코레이터 | 클래스의 정의를 읽거나 수정 | constructor | .d.ts 파일, declare 클래스 |
메서드 데코레이터 | 메서드의 정의를 읽거나 수정 | target, propertyKey, propertyDescriptor | .d.ts 파일, declare 클래스, 오버로드 메서드 |
접근자 데코레이터 | 접근자의 정의를 읽거나 수정 | target, propertyKey, propertyDescriptor | .d.ts 파일, declare 클래스 |
속성 데코레이터 | 속성의 정의를 읽음 | target, propertyKey | .d.ts 파일, declare 클래스 |
매개변수 데코레이터 | 매겨변수의 정의를 읽음 | target, propertyKey, parameterIndex | .d.ts 파일, declare 클래스 |
- 매개 변수, 메서드 데코레이터에 이어서 접근자, 속성 데코레이터가 각 인스턴스 멤버에 적용된다.
- 평가 순서는 메서드/접근자/속성 → 매개변수
- 매개 변수 데코레이터에 이어서 메서드, 접근자, 속성 데코레이터가 각 정적 멤버에 적용된다.
- 평가 순서는 메서드/접근자/속성 → 매개변수
- 매개 변수 데코레이터가 생성자 > 클래스 데코레이터가 클래스에 적용된다.
- 평가 순서는 클래스 → 생성자
참고 링크
https://velog.io/@octo__/TypeScript-데코레이터Decorator#데코레이터-함수-선언
https://www.typescriptlang.org/ko/docs/handbook/decorators.html
함께보면 좋은 글
https://www.centbin.com/never-%ed%83%80%ec%9e%85%ec%97%90-%eb%8c%80%ed%95%9c-%ec%a0%95%eb%a6%ac/