BackEnd/NodeJs

[NestJs] 2- 데코레이터

728x90

NestJs 는 데코레이터를 적극 활용한다.

 

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

 

예시를 한번 보자. 상당히 스프링처럼 생겼다.. (필자는 스프링을 다룰 줄 모른다)

 

class CreateUserDto {
  @IsEmail()
  @MaxLength(60)
  readonly email: string;

  @IsString()
  @Matches(/^[A-Za-z\d!@#$%^&*()]{8,30}$/)
  readonly password: string;
}

당황스럽지만 차근차근 봐보자.

 

위 코드는 사용자가 요청을 제대로 보냈는지 검사하는 코드이다.

 

email은 IsEmail()을 통해 이메일 형식을 정의하고 있고, 최대 길이를 60자로 정의하고 있다. (MaxLength)

 

password또한 문자열로 되어있어야 하고, 주어진 정규표현식에 적합해야함을 명시하고 있다.

 

 

데코레이터는 타입스크립트 스펙에선 아직 실험적인 기능이므로 tsconfig.json 파일에서

 

{
  "compilerOptions": {
        ...
    "experimentalDecorators": true,
        ...
  }
}

 

experimentalDecorators 을 true로 설정해줘야 데코레이터를 사용할 수 있다.

 

 

데코레이터는 아래와 같이 사용할 수 있다.

function deco(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('데코레이터가 평가됨');
}

class TestClass {
  @deco
  test() {
    console.log('함수 호출됨')
  }
}

const t = new TestClass();
t.test();

 

결과는 아래와 같다.

데코레이터가 평가됨
함수 호출됨

 

TestClass객체를 만든 후에, test함수를 실행하면, 데코레이터를 통해 평가를 하고 test()코드를 진행하는것을 알 수 있다.

 

 

 

데코레이터에 인자를 넘기려면 데코레이터 팩토리(데코레이터를 리턴하는함수)를 만들면 된다.

 

function deco(value: string) {
  console.log('데코레이터가 평가됨');
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(value);
  }
}

class TestClass {
  @deco('HELLO')
  test() {
    console.log('함수 호출됨')
  }
}

 

데코레이터가 평가됨
HELLO
함수 호출됨

 

 

 

여러개의 데코레이터를 사용하는 경우는 어떨까?

 

데코레이터를 여러개 사용하면 수학에서의 함수합성과 같이 사용된다.

 

@f
@g
test

위와 같이 데코레이터를 선언한다면, f(g(x)) 와 같이 합성된다.

 

  1. 각 데코레이터의 표현은 위에서 아래로 평가(evaluate)된다.
  2. 그런 다음 결과는 아래에서 위로 함수로 호출(call)된다.

말이 살짝 어려울 수 있는데, 예시 코드를 한번 보자.

 

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() {
    console.log('method is called');
  }
}

 

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
method is called

먼저 first => second 순서로 함수가 호출되고, 리턴되는 데코레이터는 나중에 선언되었던 second의 데코레이터부터 실행되는것을 확인할 수 있다.

 

 

 

클래스 데코레이터

 

클래스 데코레이터는 이름 그대로 클레스 앞에 선언되는 데코레이터이다.

function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    reportingURL = "http://www.example.com";
  };
}

@reportableClassDecorator
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
    this.title = t;
  }
}

const bug = new BugReport("Needs dark mode");
console.log(bug);

 

클래스 데코레이터 팩토리는 생성자 타입(new (...args: any[]): {})을 상속받는 제네릭 타입 T를 가지는 생성자를 인자로 전달하고 있다.

 

클래스 데코레이터는 생성자를 리턴하는 함수여야 한다.

 

위 예시에서는 reportingURL이라는 새 속성을 추가하고 있다.

 

{type: 'report', title: 'Needs dark mode', reportingURL: 'http://www.example.com'}

 

 

 

메서드 데코레이터

 

메서드 데코레이터는 메서드 바로앞에 선언된다. 아래 3개의 인수를 가진다.

 

  1. 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
  2. 멤버의 이름
  3. 멤버의 속성 디스크립터. PropertyDescriptor 타입을 가짐.
function HandleError() {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target) //{constructor: ƒ, greet: ƒ}
    console.log(propertyKey) //hello
    console.log(descriptor) //value: ƒ, writable: true, enumerable: false, configurable: true}

    const method = descriptor.value; //원래 정의된 메서드 저장

    descriptor.value = function() {
      try { 
        method();  //원래 메서드 호출
      } catch (e) {
        // 에러 핸들링 로직 구현
        console.log(e);
      }
    }
  };
}

class Greeter {
  @HandleError()
  hello() {
    throw new Error('테스트 에러');
  }
}

const t = new Greeter();
t.hello();

 

PropertyDescriptor는 객체 속성의 특성을 기술하는 객체이다.

아래와 같은 속성들을 지니고 있다.

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

 

 

접근자 데코레이터

접근자 바로앞에 선언되며, 접근자의 정의를 읽거나 수정할 수 있다.

반환하는 값은 해당 멤버의 속성 디스크립터가 된다.

 

이 역시 예시를 한번 보면 이해가 될 것이다.

 

 

function Enumerable(enumerable: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = enumerable; //enumerable 속성을 데코레이터의 인자로 결정
  }
}

class Person {
  constructor(private name: string) {} //name을 private로 설정(외부접근 x)

  @Enumerable(true)
  get getName() {
    return this.name;
  }

  @Enumerable(false)
  set setName(name: string) {
    this.name = name;
  }
}

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

위 코드를 실행하면 setName은 열거하지 못하게 되어있기대문에 for문에서 key로 받을 수가 없다.

 

 

 

속성 데코레이터

 

클래스의 속성 바로앞에 선언된다. 인수로는 2개를 가진다!

 

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

다른 데코레이터와 다르게 속성 디스크립터가 존재하지 않다. 공식문서에 따르면 반환값도 무시되는데, 이는 현재 프로토타입의 멤버를 정의할때 인스턴스 속성을 설명하는 메커니즘이 없고, 속성의 초기화 과정을 관찰 혹은 수정할 수 있는 방법이 없기 때문이라고 한다.

function format(formatString: string) {
  return function (target: any, propertyKey: string): any {
    let value = target[propertyKey];

    function getter() {
      return `${formatString} ${value}`;
    }

    function setter(newVal: string) {
      value = newVal;
    }

    return {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    }
  }
}

class Greeter {
  @format('Hello')
  greeting: string;
}

const t = new Greeter();
t.greeting = 'World';
console.log(t.greeting);

 

 

 

이제 마지막이다!

 

매개변수 데코레이터

이름에서 이젠 알 수 있듯이 생성자 혹은 메서드의 파라미터에 선언된다. 인수로는 3개를 가진다.

 

 

  1. 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
  2. 멤버의 이름
  3. 매개변수가 함수에서 몇 번째 위치에 선언되었는 지를 나타내는 인덱스

보통 Nest에서 API 요청 파라미터에 대해 유효성 검사를 하는 경우 아래 예시와 유사한 데코레이터를 많이 사용한다.

 

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

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;

  descriptor.value = function(...args) {
    Object.keys(target.validators).forEach(key => {
      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('Dexter');
console.log('----------')
t.setName('De');

 

 

예시를 읽다보면 데코레이터에 대한 개념을 희미하게나마 이해할 수 있다. 이를 실제로 적용하는것은 별개의 문제겠지만..?

 

데코레이터를 활용하면 검사, 판별을 조금 더 효율적으로 할 수 있을거란 생각이 들었다.

728x90