Decorator에 대한 기초와 활용 – 1

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) ****에 적용되며 메서드 정의를 관찰, 수정 또는 대체하는 데 사용할 수 있습니다. 메서드 데코레이터는 선언 파일, 오버로드 또는 기타 주변 컨텍스트(예: 선언 클래스)에서 사용할 수 없습니다.

메서드 데코레이터의 표현식은 런타임에 다음 세 개의 인수와 함께 함수로 호출됩니다:

  1. 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입 입니다.
  2. 멤버의 이름
  3. 멤버의 프로퍼티 설명자

메서드 데코레이터가 값을 반환하면, 메서드의 프로퍼티 설명자 로 사용됩니다.

Property Descriptor
는 객체의 프로퍼티들을 기존보다 정교하게 정의할 수 있는 ES5의 스펙이며, 프로퍼티의 특성을 설명하는 역할을 하는 객체이다. 이 Property Descriptor는 Object.getOwnPropertyDescriptor를 사용해서 가져올 수 있다.

아래 코드는 메서드에 적용된 메서드 데코레이터입니다.

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 키워드가 있습니다.

접근자 데코레이터의 표현 식은 세 가지 인수를 전달 받습니다.

  1. static 프로퍼티가 속한 클래스의 생성자 함수 또는 인스턴스 프로퍼티에 대한 클래스의 프로토 객체
  2. 해당 메서드의 이름
  3. 해당 메서드의 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 객체를 반환함으로써 프로퍼티 설정을 바꿀 수 있습니다.

프로퍼티 데코레이터에는 두 개의 인수가 존재합니다.

  1. 정적 멤버에 대한 클래스 생성자 함수나 인스턴스 멤버에 대한 클래스 프로토타입
  2. 멤버의 이름

아래는 프로퍼티 데코레이터 예시 코드입니다.

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) 데코레이터

파라미터에 직접적으로 선언하는 데코레이터로써 클래스 생성자 또는 메서드 선언 함수에 적용됩니다.

사용 되는 인수는 세 개 입니다.

  1. 클래스의 생성자 함수 또는 프로퍼티 객체
  2. 메서드의 이름
  3. 매개변수 순서 인덱스

아래는 매개변수 데코레이터 예시 코드 입니다.

// 매개 변수 데코레이터 함수
// 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 클래스
  1. 매개 변수, 메서드 데코레이터에 이어서 접근자, 속성 데코레이터가 각 인스턴스 멤버에 적용된다.
    • 평가 순서는 메서드/접근자/속성 → 매개변수
  2. 매개 변수 데코레이터에 이어서 메서드, 접근자, 속성 데코레이터가 각 정적 멤버에 적용된다.
    • 평가 순서는 메서드/접근자/속성 → 매개변수
  3. 매개 변수 데코레이터가 생성자 > 클래스 데코레이터가 클래스에 적용된다.
    • 평가 순서는 클래스 → 생성자

참고 링크

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/

Leave a Comment