NestJS에서 데코레이터 활용하기

데코레이터

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

☑️ 타입스크립트의 데코레이터는 파이썬의 데코레이터나 자바의 어노테이션과 유사한 기능을 한다.

☑️ 클래스, 메서드, 접근자, 프로퍼티, 매개변수에 적용 가능하다. 각 요소의 선언부 앞에 @로 시작하는 데코레이터를 선언하면 데코레이터로 구현된 코드를 함께 실행한다.

☑️ 예를 들어 다음 코드는 유저 생성 요청의 본문을 DTO로 표현한 클래스이다.

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

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

☑️ 사용자는 얼마든지 요청을 잘못 보낼 수 있으므로 데코레이터를 이용하여 애플리케이션이 허용하는 값으로 제대로 요청을 보냈는지 검사하고 있다.

  • email은 이메일 형식을 가진 문자열이어야 하고 → @IsEmail()
  • 그 길이는 최대 60자이어야 한다. → @MaxLength(60)
  • password는 문자열이어야 하고 → @IsString()
  • 주어진 정규 표현식에 적합해야 한다. → @Matches(/^[A-Za-z\d!@#$%^&*()]{8,30}$/)

☑️ 데코레이터는 @expression 형식으로 사용된다. 여기서 expression은 데코레이팅 된 선언 (데코레이터가 선언되는 클래스, 메서드 등)에 대한 정보와 함께 런타임에 호출되는 함수여야 한다.

다음과 같은 메서드 데코레이터가 있고 이 데코레이터는 test라는 메서드에 선언했다. 여기서 deco 함수에 인자들이 있는데 메서드 데코레이터로 사용하기 위해서 이렇게 정의해야 한다.

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

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

const t = new TestClass();
t.test();
-- 콘솔 출력
데코레이터가 평가됨
함수 호출됨

데코레이터 함성

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

다음 데코레이션 선언의 합성 결과는 $f(g(x))$와 같다

@f
@g
test

여러 데코레이터를 사용할 때 다음 단계가 수행된다.

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

다음 예시의 출력 결과를 보면 합성 순서에 대한 이해를 높일 수 있다.

function first() {
  console.log("first(): factory evalutaed");
  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

타입스크립트에서 지원하는 5가지 데코레이터

클래스 데코레이터 (Class Decorator)

☑️ 클래스 바로 앞에 선언된다.

☑️ 클래스 데코레이터는 클래스의 생성자에 적용되어 클래스 정의를 읽거나 수정할 수 있다.

☑️ 선언 파일과 선언 클래스 내에서는 사용이 불가하다.

다음 코드는 클래스에 reportingURL 속성을 추가하는 클래스 데코레이터의 예이다.

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

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

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

const bug = new BugReport("Needs dark mode");
console.log(bug);
--콘솔출력
{type: 'report', title: 'Needs dark mode', reportingURL: 'http://www.example.com'}
  • 클래스 데코레이터 팩토리이다. (1)
    • 생성자 타입(new (…args: any[]): {}. new 키워드와 함께 어떠한 형식의 인자들도 받을 수 있는 타입)을 상속받는 제네릭 타입 T 를 가지는 생성자(constructor)를 팩토리 메서드의 인자로 전달하고 있다.
  • 클래스 데코레이터는 생성자를 리턴하는 함수여야 한다. (2)
  • 클래스 데코레이터가 적용되는 클래스에 새로운 reportingURL이라는 속성을 추가한다. (3)
  • BugReport 클래스의 선언되어 있지 않은 새로운 속성이 추가됐다.

메서드 데코레이터 (Method Decorator)

☑️ 메서드 바로 앞에 선언된다.

☑️ 메서드의 속성 디스크립터에 적용되고 메서드의 정의를 읽거나 수정할 수 있다.

☑️ 선언 파일, 오버로드 메서드, 선언 클래스에 사용이 불가하다.

메서드 데코레이터는 다음 세 가지의 인수를 가진다.

  • 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
  • 멤버의 이름
  • 멤버의 속성 디스크립터 → PropertyDescriptor 타입을 가짐

만약 메서드 데코레이터가 값을 반환한다면 이는 해당 메서드의 속성 디스크립터가 된다.

메서드 데코레이터의 예이다. 함수를 실행하는 과정에서 에러 발생시 이 에러를 잡아서 처리하는 로직이다.

function HandleError() {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { // 1
    console.log(target); // 2
    console.log(propertyKey); // 3
    console.log(descriptor); // 4

    const method = descriptor.value; // 5

    descriptor.value = function() { 
      try {
        method(); // 6
      } catch (e) {
        // error handling logic
        console.log(e);
      }
    }
  };
}

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

const t = new Greeter();
t.hello();
  • 메서드 데코레이터가 가져야 하는 3개의 인자이다. (1)
    • PropertyDescriptor는 객체 속성의 특성을 기술하고 있는 객체로, enumerable 외에도 여러가지 속성을 지닌다.
    • enumerable이 true면 이 속성을 열거형이라는 뜻이 된다.
      interface PropertyDescriptor {
      	configurable?: boolean;
      	enumerable?: boolean;
      	value?: any;
      	writable?: boolean;
      	get?(): any;
      	set?(v: any): void;
      }
    
  • target의 출력 결과는 {constructor: f, greet: f}이다. (2)
    • 데코레이터가 선언된 메서드 hello가 속해있는 클래스의 생성자와 프로토타입을 가지는 객체임을 알 수 있다.
  • propertyKey의 출력 결과는 함수이름 hello이다. (3)
  • hello 함수가 처음 가지고 있던 디스크립터가 출력된다. descriptor의 출력 결과는 {value: f, writable: true, enumerable: false, configurable: true} 이다. (4)
  • 디스크립터의 value 속성으로 원래 정의된 메서드를 저장한다. (5)
  • 원래의 메서드 호출 (6)

접근자 데코레이터 (Access Decorator)

☑️ 접근자 바로 앞에 선언한다.

☑️ 접근자의 속성 디스크립터에 적용되고 접근자의 정의를 읽거나 수정할 수 있다.

☑️ 선언 파일과 선언 클래스에 사용이 불가하다.

☑️ 접근자 데코레이터가 반환하는 값은 해당 멤버의 속성 디스크립터가 된다.

특정 멤버를 열거가 가능한지 결정하는 데코레이터의 예이다.

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

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

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

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

// 5
const person = new Person("Dexter");
for (let key in person) {
  console.log(`${key}: ${person[key]}`);
}
-- 콘솔 출력
name: Dexter
getName: Dexter
  • 디스크립터의 enumerable 속성을 데코레이터의 인자로 결정한다. (1)
  • name은 외부에서 접근하지 못하는 private 멤버이다. (2)
  • 게터 getName 함수는 열거가 가능하도록 한다. (3)
  • 세터 setName 함수는 열거가 불가능하도록 한다. (4)
  • 출력 결과는 getName은 출력되지만, setName은 열거하지 못하기 때문에 for문에서 key로 받을 수 없다. (5)

속성 데코레이터 (Property Decorators)

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

☑️ 선언 파일, 선언 클래스에서 사용하지 못한다.

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

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

☑️ 메서드 데코레이터나 접근자 데코레이터와 비교했을 때 세 번째 인자인 속성 디스크립터가 존재하지 않는다.

☑️ 공식문서에 따르면 반환값도 무시되고, 이는 현재 프로토타입의 멤버를 정의할 때 인스턴스 속성을 설명하는 메커니즘이 없고 속성의 초기화 과정을 관찰하거나 수정할 수 있느 방법이 없기 때문이라고 한다.

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") // 1
  greeting: string;
}

const t = new Greeter();
t.greeting = "World";
console.log(t.greeting);
-- 콘솔 출력
Hello World
  • 데코레이터에 formatString 전달 (1)

매개변수 데코레이터 (Parameter 데코레이터)

☑️ 생성자 또는 메서드의 파라미터에 선언되어 적용된다.

☑️ 선언 파일, 선언 클래스에 사용이 불가하다.

매개변수 데코레이터는 호출될 때 3가지의 인자와 함께 호출된다. (반환값은 무시된다.)

  • 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
  • 멤버의 이름
  • 매개변수가 함수에서 몇 번째 위치에 선언되었는지를 나타내는 인덱스
import { BadRequestException } from "@nestjs/common";

function MinLength(min: number) { // 1
  return function (target: any, propertyKey: string, parameterIndex: number) {
    // 2
    target.validators = {
      minLength: function (args: string[]) { // 3
        return args[parameterIndex].length >= min; // 4
      }
    }
  }
}

function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { // 5
  const method = descriptor.value; // 6

  descriptor.value = function(...args) { // 7
		// 8
    Object.keys(target.validators).forEach(key => {
      if (!target.validators[key](args)) {
        throw new BadRequestException();
      }
    })
    method.apply(this, args); // 9
  }
}

class User {
  private name: string;

  @Validate
  setName(@MinLength(3) name: string) {
    this.name = name;
  }
}

const t = new User();
t.setName("Dexter"); // 10
console.log("------------");
t.setName("De"); // 11
  • 파라미터의 최솟값을 검사하는 파라미터 데코레이터 (1)
  • target 클래스 (여기서는 User)의 validators 속성에 유효성을 검사하는 함수를 할당한다. (2)
  • args 인자는 (7)에서 넘겨받은 메서드의 인자이다. (3)
  • 유효성 검사를 위한 로직이다. parameterIndex에 위치한 인자의 길이가 최솟값보다 같거나 큰지 검사한다. (4)
  • 함께 사용할 메서드 데코레이터 (5)
  • 메서드 데코레이터가 선언된 메서드를 method 변수에 임시 저장한다. (6)
  • 디스크립터의 value에 유효성 검사 로직이 추가된 함수를 할당한다. (7)
  • target(User 클래스)에 저장해둔 validators를 모두 수행한다. (8)
    • 이 때 원래 메서드에 전달된 인자(args)들을 각 validator에 전달한다.
  • 원래 함수를 실행한다 (9)
  • 파라미터 name의 길이가 5이므로 문제 없다. (10)
  • 파라미터 name의 길이가 3보다 작으므로 BadRequestException이 발생한다. (11)

5가지 데코레이터 특징 정리

데코레이터 역할 호출시 전달되는 인자 선언 불가능한 위치
클래스 데코레이터 클래스의 정의를 읽거나 수정 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 클래스