본문 바로가기

Backend/NestJS

[NestJS] 데커레이터 Decorator(합성, 클래스, 메서드, 매개변수)

728x90
Decorator(@)에 대한 공부 기록이다.

개요

NestJS는 Decorator를 적극 활용하는 프레임워크다.
때문에 이에 대한 이해는 필수적이다.

데커레이터를 활용하면 횡단 관심사를 분리하여 관점 지향 프로그래밍을 적용한 코드를 작성할 수 있다.

각 요소의 선언부 앞에 @로 시작하는 데커레이터를 선언하면 데커레이터로 구현된 코드를 함께 실행한다.

관점 지향 프로그래밍
횡단 관심사의 분리(다른 관심사에 영향을 미치는 코드를 분리)를 허용하여
모듈성을 증가시키는 것이 목적인 프로그래밍 방식
//tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
  }
}

위와 같이,

tsconfig.json 파일 내의 experimentalDecorators 값이 true여야 데커레이터를 사용할 수 있다.

 

이번 기록의 방향성은 데커레이터를 정의하는 방법에 대해 알아보고 데커레이터의 합성이 어떻게 이뤄지는지 본 후,

타입스크립트가 지원하는 5가지 데커레이터에 대해 기록할 것이다.(클래스, 메서드, 접근자, 속성, 매개변수)

 

데커레이터 정의

function deco(
  target: any,
  propertyKey: string,
  description: PropertyDescriptor,
) {
  console.log('데커레이터');
}

class TestClass {
  @deco
  test() {
    console.log('함수 실행');
  }
}
const t = new TestClass();
t.test();

코드의 데커레이터는 이후에 볼 메서드 데커레이터이다. 이는 위와 같이 정의되며, 코드를 실행하면

콘솔에 사진과 동일한 로그가 찍히는 것을 볼 수 있다.

데커레이터의 구성요소에 대해서는 5가지 종류를 설명할 때 각각 자세히 다뤄보자.

데커레이터 합성

데커레이터를 실행할 때는 2가지 단계를 거치게 된다.

  1. 평가 (evaluate) : 데커레이터에서 반환값을 반환하기 전
  2. 호출 (call) : 데커레이터의 반환값

데커레이터의 합성은 수학의 함수 합성과 동일한 방법으로 이루어진다.

따라서 평가 단계는 위에서 아래, 호출 단계는 아래에서 위로 수행된다.

function f() {
  console.log('first evaluate');
  return function (
    target: any,
    propertyKey: string,
    description: PropertyDescriptor,
  ) {
    console.log('first called');
  };
}

function g() {
  console.log('second evaluate');
  return function (
    target: any,
    propertyKey: string,
    description: PropertyDescriptor,
  ) {
    console.log('second called');
  };
}

class TestClass {
  @f()
  @g()
  test() {
    console.log('method call');
  }
}
const t = new TestClass();
t.test();

코드를 실행하니 평가는 위에서 아래, 실행은 아래에서 위로 되는 것을 확인할 수 있다.


5가지 데커레이터

이제 5가지 데커레이터에 대해 자세히 알아보자.

 

클래스 데커레이터

클래스 바로 앞에 선언되는 데커레이터이다.
클래스의 생성자에 적용되어 클래스 정의를 읽거나 수정할 수 있다.

정의 파일과 정의 클래스 내에서는 사용할 수 없다.

아래는 정의 파일에 대한 기록이다.

 

[TS] tsconfig.json,정의 파일(d.ts),JSDocs

tsconfig.json파일, 정의 파일(d.ts)과 JSDocs에 대해 공부한 기록을 남기려고 한다. tsconfig.json 먼저 프로젝트를 생성해준다. $ npm init -y 그리고 tsconfig.json 파일을 생성해준다. //tsconfig.json { "include": ["src"

choi-records.tistory.com

 

아래 코드는 생성한 클래스에 reportingURL 속성을 추가하는 클래스 데커레이터의 예시이다.

function reportableClassDecorator<T extends { new (...args: any[]) }>(constructor: T)
//클래스 데커레이터 팩터리이다. 생성자 타입을 상속받는 제너릭 타입 T를 가지는 생성자를 팩터리 메서드의 인수로 전달하고 있다.
{
  return class extends constructor {
  //클래스 데커레이터는 생성자를 리턴하는 함수여야 한다.
    reportingURL = 'http://www.example.com';
    //해당 클래스 데커레이터가 적용되는 클래스에 새로운 reportingURL이라는 새로운 속성을 추가한다.
  };
}

@reportableClassDecorator
class TestClass {
  type = 'report';
  title = 'test';
}

const t = new TestClass();
console.log(t);

코드 주석에서 볼 수 있듯 클래스 데커레이터는 생성자를 반환하는 함수여야 한다.

 

메서드 데커레이터

메서드 바로 앞에 선언된다.
메서드의 서술자(속성의 특성을 설명하는 객체)에 적용되고, 메서드의 정의를 읽거나 수정할 수 있다.

앞서 만들었던 deco 데커레이터에서 볼 수 있듯

메서드 데커레이터는 3가지의 인수를 가진다.

  1. target : 정적 멤버가 속한 클래스의 생성자 함수이거나, 인스턴스 멤버에 대한 클래스의 포로토타입
  2. propertyKey : 멤버의 이름
  3. description : 멤버의 속성 설명자. type은 PropertyDescriptor이다.
function HandleError() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  )
  //메서드 데커레이터가 가져야 하는 3개의 인수
  {
    console.log(target);
    //{constructor : f, hello : f}
    console.log(propertyKey);
    //'hello'
    console.log(descriptor);
    /*
    {
  value: [Function: hello],
  writable: true,
  enumerable: false,
  configurable: true
  }
  */
    const method = descriptor.value;
	//원래 정의된 method를 변수에 저장
    descriptor.value = function () {
      try {
        method();
        //method 실행
      } catch (e) {
        console.log(e);
        //error handling logic
        //test error 출력
      }
    };
  };
}

class Greeter {
  @HandleError()
  hello() {
    throw new Error('test Error');
  }
}

const t = new Greeter();
t.hello();
  • PropertyDescriptor의 interface를 보자.
interface PropertyDescriptor {
    configurable?: boolean;
    //속성의 정의를 수정할 수 있는지 여부
    enumerable?: boolean;
    //열거형인지 여부
    value?: any;
    //속성 값
    writable?: boolean;
    //수정 가능 여부
    get?(): any;
    //getter
    set?(v: any): void;
    //setter
}

접근자 데커레이터

접근자 데커레이터는 접근자 바로 앞에 선언한다.
여기서 접근자는 getter와 setter 함수를 말한다.(객체 프로퍼티에 접근할 수 있는 메서드)

예제로 객체 프로퍼티에 접근하는 접근자를 생성하고

접근자 각각의 enumarable을 설정함으로써 getter 함수를 열거 가능하게 하고,

setter 함수는 열거가 불가능하게 수정해 보자.

function Enumerable(enumerable: boolean) 
//enumerable 값을 인자로 받도록 한다.
{
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    descriptor.enumerable = enumerable;	
  };
}

class Person {
  constructor(public name: string) {}

  @Enumerable(true)
  //getter 메서드는 열거 가능하게 설정
  get getName() {
    return this.name;
  }

  @Enumerable(false)
  //setter 메서드는 열거 불가능하게 설정
  set setName(name: string) {
    this.name = name;
  }
}

const person = new Person('Choi');
for (let key in person) {
  console.log(`${key}: ${person[key]}`);
}

결과는 아래와 같다.

속성 데커레이터

클래스의 속성 바로 앞에 선언된다.

속성 데커레이터는 두 개의 인수를 가지는 함수이다.

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

메서드 데커레이터의 target과 propertyKey를 인수로 가진다.

function format(formatString: string) {
  return function (target: any, propertyKey: string): any {
    let value = target[propertyKey];
    function getter() {
      return `${formatString} ${value}`;
      //getter에서 데커레이터 인수로 들어온 formatString을 원래 속성과 조합한 스트링으로 바꿈
    }
    function setter(newVal: string) {
      value: newVal;
    }
    return {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    };
  };
}

class Greeter {
  @format('Hello')
  //formString 전달
  greeting: string;
}

const t = new Greeter();
t.greeting = 'World';
console.log(t.greeting);
//getter 호출에 따라 Hello World 출력

매개변수 데커레이터

생성자 또는 메서드의 매개변수에 선언되어 적용

3가지 인수를 가진다.

  1. target
  2. propertyKey
  3. parameterIndex : 매개변수가 함수에서 몇 번째 위치에 선언되었는지를 나타내는 인덱스

유효성을 검사하는 예제를 통해 매개변수 데커레이터를 사용해 보자.

import { BadRequestException } from '@nestjs/common/exceptions';

function MinLength(min: number) {
  //매개변수의 최솟값을 검사하는 매개변수 데커레이터
  return function (target: any, propertyKey: string, parameterIndex: number) {
    target.validators = {
      //target class의 validators 속성에 유효성 검사 함수를 할당
      minLength: function (args: string[]) {
        //args 인수는 Validate 메소드에서 넘겨받을 인수
        return args[parameterIndex].length >= min;
        //유효성 검사 결과를 반환
      },
    };
  };
}

function Validate(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor,
) {
  const method = descriptor.value;
  //데커레이터가 선언된 메소드를 변수에 저장
  descriptor.value = function (...args) {
    //value에 유효성 검사 로직이 추가된 함수 할당
    Object.keys(target.validators).forEach((key) => {
      //target에 저장해둔 validators를 모두 수행. 이때 원래 메서드에 전달된 인수들은 각 validator에 전달
      if (!target.validators[key](args)) {
        throw new BadRequestException();
      }
    });
    method.apply(this, args);
  };
}

class User {
  private name: string;
  @Validate
  setName(@MinLength(3) name: string) {
    this.name = name;
  }
}
const t = new User();
t.setName('Choi');
console.log('------------------');
t.setName('De');

 

728x90