(NestJS-기초강의) 5. 데코레이터 (Decorator)

NestJS에서 데코레이터는 적극 활용되는데, 이번 포스팅에서는 데코레이터에 대해 알아보자.

데코레이터란?

NestJS에서 데코레이터는 기능을 추가하거나 수정하는 도구로, 클래스, 메서드, 접근자, 프로퍼티 등의 요소에 특별한 기능을 부여하는 역할을 합니다.

이를 통해 코드를 깔끔하게 유지하고 관점 지향 프로그래밍을 할 수 있습니다.

장점

  • 모듈화와 응집성 강화: 데코레이터를 활용하면 기능을 요소에 간단하게 적용하여 모듈화하고 코드를 응집시킬 수 있습니다.
  • 가독성 향상: 코드에 필요한 기능을 선언부에 직관적으로 표현하여 가독성을 높여줍니다.
  • 유연성 및 확장성: 코드의 유연성을 높여주며, 새로운 기능 추가나 변경을 용이하게 합니다.
  • 횡단 관심사 분리: 횡단 관심사(cross-cutting concern)를 분리하여 관리하고 , 수정할 때 다른 코드 영향을 최소화할 수 있습니다.

단점

  • 복잡성: 데코레이터의 복잡한 사용법 및 다양한 옵션을 이해하기에는 시간과 노력이 필요합니다.
  • 실험적 기능: 데코레이터는 일부 경우 실험적인 기능으로, 안정성이나 호환성에 대한 문제가 있을 수 있습니다.
  • 오용 가능성: 잘못된 사용으로 인해 코드의 복잡성이 높아지고 유지보수가 어려워질 수 있습니다.

데코레이터 적용

사용법

각 요소의 선언 앞에 @ 기호를 사용하여 선언되어 코드와 함께 시행됩니다.

먼저 이를 이용하기 위해서는 tsconfig.json파일에 다음과 같이 설정되어 있어야 합니다.

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

다음 예제코드는 DTO(Data Transfer Object)를 정의하는 예제로 @IsEmail(), @MaxLength(), @IsString(), @Matches()와 같은 데코레이터를 사용하여 각 데이터를 검증하는 기능을 추가했습니다.

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

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

다음과 같이 method 형태로 정의하여 사용할 수 있습니다.

function deco(value: string) {
  console.log('evaluated');
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    console.log(value);
  };
}

class TestClass {
  @deco('HELLO')
  test() {
    console.log('called this function');
  }
}
데코레이터 합성(Decorator Composition)

데코레이터는 하나의 멤버에 여러 데코레이터를 사용할 수 있으며 이를 합성(Decorator Composition)이라 부릅니다.

수하적으로 f(g(x))를 표현할 때 다음과 같이 사용합니다.

@f
@g
x

다음과 같이 구현하게 되면

function f() {
  console.log("f(): Start");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("f(): Evaluated");
  };
}

function g() {
  console.log("g(): Start");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("g(): Evaluated");
  };
}

class ExampleClass {
  @f()
  @g()
  test() {
    console.log('test() is called');
  }
}

이 때, ExampleClass의 test()를 호출하게 되면 다음과 같은 결과를 얻습니다.

f(): Start
g(): Start
g(): Evaluated
f(): Evaluated
test() is called

데코레이터 종류

Class Decorator

클래스 전체에 적용되며 클래스를 수정, 변경하거나 확장하는 데 사용됩니다.

예제)

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T,
) {
  // Inherit the constructor of the Test class
  // Set the added constructor to be executed when a new Test() is created.
  return class extends constructor {
    testValue = 'override';
    newValue = 'new property';
  };
}

@classDecorator
class Test {
  testValue: string;

  constructor(m: string) {
    this.testValue = m;
  }
}

  const test = new Test('test');

  console.log(test.testValue);
  console.log(test);

  // but can't use test.newValue
  // console.log(test.newValue);

위 예제에서 class decorator를 이용하여, 생성자를 팩토리 method의 인자로 전달하여 새로운 속성인 newValue를 추가하였고, class 출력결과에서 추가된 속성을 확인할 수 있습니다.

다만 class의 타입이 변경되는 것은 아니므로 추가된 속성을 test.newValue와 같은 식으로 사용하지는 못합니다.

override
Test { testValue: 'override', newValue: 'new property' }

Method Decorator

클래스 내부의 메서드에 적용되며 메서드 동작을 수정하거나 추가적인 동작을 주입하는 데 사용됩니다.

총 3개의 매개변수를 갖습니다.

  • target: 데코레이터가 적용된 메서드의 속성을 포함하는 클래스에 대한 참조입니다.
    이를 통해 클래스의 인스턴스를 조작하거나 클래스 자체를 조작할 수 있습니다.
  • propertyKey: 데코레이터가 적용된 메서드의 이름입니다.
    메서드의 식별자로 사용되며, 해당 메서드에 대한 정보를 얻거나 수정할 때 활용됩니다.
  • descriptor: 메서드에 대한 프로퍼티 설명자(Property Descriptor)입니다.
    이를 통해 메서드의 속성을 조작하거나 변경할 수 있습니다.
    메서드의 구성 정보를 담고 있어서, 메서드의 동작을 수정하는 데 사용될 수 있습니다.

예제)

function MethodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('MethodDecorator called on:', target, propertyKey, descriptor);
}

class ExampleClass {
  @MethodDecorator
  exampleMethod() {
    // logic
  }
}

Accessor Decorator

클래스의 getter 또는 setter에 적용되며 해당 속성에 대한 접근을 수정하거나 추가적인 동작을 부여하는 데 사용됩니다.

예제)

function Enumerable(enumerable: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = enumerable;
  }
}

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

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

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

Property Decorator

클래스의 속성에 적용되며 속성에 대한 추가적인 설정 또는 동작을 부여하는 데 사용됩니다.

총 2개의 매개변수를 사용합니다.

  • target: 데코레이터가 적용된 속성이 포함된 클래스에 대한 참조입니다.
    이를 통해 클래스 자체를 조작하거나 속성에 접근하여 속성을 조작할 수 있습니다.
  • propertyKey: 데코레이터가 적용된 속성의 이름(키)입니다. 해당 속성에 대한 정보를 가져오거나 수정하는 데 사용됩니다.

예제)

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);

Parameter Decorator

함수 또는 메서드의 매개변수에 적용되며 매개변수에 대한 추가적인 정보를 제공하거나 수정하는 데 사용됩니다.

단독으로 보다 method decorator와 함께 사용됩니다.

아래 예제에서 @MinLength()가 parameter decorator에 해당합니다.

다음의 3개의 매개변수를 사용합니다.

  • target: 데코레이터가 적용된 매개변수가 속한 함수의 프로토타입을 가리킵니다. 이를 통해 함수에 대한 정보를 가져오거나 수정할 수 있습니다.
  • propertyKey: 매개변수가 속한 메서드의 이름입니다. 해당 메서드에 대한 정보를 가져오거나 조작하는 데 사용됩니다.
  • parameterIndex: 데코레이터가 적용된 매개변수의 인덱스입니다. 함수 내에서 매개변수의 위치를 나타냅니다. 이를 통해 특정 매개변수에 접근하거나 수정할 수 있습니다.

예제)

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;
  }
}

참고링크

Leave a Comment