[Nest.js]데커레이터

Donghun Seol·2023년 4월 6일
0

데커레이터

데커레이터 역시 확장에는 열리고 변경에는 닫혀있다는 OCP를 준수하는 패턴이라 생각하는데 기존의 로직을 바꾸지 않고 DRY를 준수하면서 새로운 기능을 추가할 수 있기 때문이다.(뇌피셜)

기본 정의

데커레이터는 함수,함수,함수다. 이 함수는 target의 동작이나 속성을 변경시킨다.
클래스, 메서드, 접근자, 속성, 매개변수 데커레이터가 있다.
(데커레이터를 잘 활용하면 횡단관심사를 분리하여 관점 지향 프로그래밍을 적용한 코드를 작성할 수 있다...지만 아직 이해는 잘 안간다.)

매개변수를 받는 데커레이터는 클로저를 형성시킨 고차함수형태로 작성한다.이 고차함수는 매개변수 없는 데커레이터 함수의 시그니쳐를 갖는 함수를 반환하게 된다. 쉽게 말하면 데커레이터 팩토리다.

// 매개변수를 받지 않는 데커레이터
function HelloWorldDeco(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
	console.log('hello world')
}

// 매개변수를 받는 고차함수 데커레이터
function HelloCustomDeco(custom: string) {
	return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    	console.log(`hello ${custom}`)
    }
}

데커레이터의 실행순서

이렇게 정리하자.

위로부터 평가한 후 스택에 넣는다.
데커레이터 스택에서 꺼내면서 호출한다.
데커레이터 스택이 비면 대상 함수를 호출한다.


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')
    }
}

클래스 데커레이터

클래스 앞에 선언되는 데커레이터로 클래스의 생성자에 적용된다.
원래의 클래스에 없는 새로운 속성을 추가할 때 사용한다.
타입에는 반영안된다는 점을 유념하자.

function ReportableClassDecorater<T extends { new (...args: any[]): object }>(
  constructor: T,
) {
  return class extends constructor {
    reportingURL = 'http://www.example.com';
  };
}
@ReportableClassDecorater
class BugReport {
  type = 'report';
  title: string;
  constructor(t: string) {
    this.title = t;
  }
}

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

메서드 데커레이터

메서드의 바로 앞에 선언되여 메서드의 property descriptor에 적용된다. 메서드를 오버로드하는데 사용할 수 있다.
아래의 예시는 handleError데커레이터로 메서드의 오류처리 로직을 구현했다. 매서드마다 동일한 오류처리가 가능하다면코드량을 상당히 줄이고 가독성을 향상시킬 수 있다. 한마디로 존멋.

function HandleError() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    console.log('target', target);
    console.log('propertyKey', propertyKey);
    console.log('descriptor', descriptor);

    const method = descriptor.value;
    descriptor.value = function () {
      try {
        method();
      } catch (error) {
        console.log('error handled');
        console.error(error.message);
      }
    };
  };
}

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

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

접근자 데커레이터

접근자의 바로 위에 선언하는 데커레이터다. 객체 프로퍼티를 숨겨야 할때 접근자를 통해 해당 프로퍼티를 읽고 쓴다. 타입스크립트에서는 get, set키워드로 접근자를 구현할 수 있다. 접근자의 위에 선언되어 접근자의 정의를 읽거나 수정한다는 것 이외에 다른 데커레이터와 크게 형식적으로 다르진 않다. 아래의 코드에서는 set, get 메서드의 enumerable 속성을 변경하는 역할을 수행해서 for..in에 출력여부를 바꾸는 데커레이터가 적용된 예시다.

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

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

  // 🤔 get은 어떻게 변경될지 생각해보자. 데커레이터의 호출 순서를 잘 기억하자!
  @Enumerable(true)
  @Enumerable(false)
  get getName() {
    return this.name;
  }

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

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

속성 데커레이터

클래스의 속성 바로 앞에 선언된다. 매개변수를 두 개만 받는 점을 유의하자.
데커레이터로 수식된 프로퍼티를 get, set 할때마다 데커레이터에 정의된 getter와 setter가 호출된다.
이를 통해 개발자가 정의한 로직에 따른 커스텀한 입출력이 가능하다.

function Format(formatString: string) {
  return function (target: any, propertKey: string): any {
    let value = target[propertKey];
    function getter() {
      console.log('getter called');
      return `Formatted Result : ${formatString} ${value}`;
    }

    function setter(newVal: string) {
      console.log('setter called with: ', newVal);
      value = newVal;
    }
    return {
      get: getter,
      set: setter,
      enumarble: true,
      configurable: true,
    };
  };
}

class Greeter {
  @Format('hello')
  greeting: string;
}

const ng = new Greeter();
// 여기서 setter가 호출된다.
ng.greeting = 'world';
// 값을 가져올 때 getter가 호출되면서 format스트링이 추가된 값을 반환한다.
console.log(ng.greeting);

매개변수 데커레이터

생성자나 메서드의 매개변수에 선언되어 적용된다. 아래 3개의 인수와 함께 호출되고 반환값은 무시된다.
가장 복잡한 형태로 구현되어있다. nest.js에서 자주 활용되므로 내부가 어떻게 돌아가는지는 기억하자.

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

잘 이해가 안되서 써보는 추가설명

MinLength 데커레이터는 target(아래의 예시에서는 name 파라미터)이라는 객체에 validators라는 속성을 삽입해준다. validator 속성은 객체인데, 그 안에 minLength라는 함수가 들어가 있다. 해당 함수는 @MinLength의 인자로 받은 min값을 대상으로 평가하는 함수이다.(아직 호출되지는 않았다.) 이후 @MinLength의 역할은 종료된다.

그 다음으로 호출되는 Validate 데커레이터는 메서드 데커레이터다. 따라서 setName()메서드의 동작을 아래 코드와 같이 확장시킨다. @MinLenght와 마찬가지로 직접적으로 호출되는건 아니고 target의 내부 동작을 확장시켜준 후 @Validate의 역할은 끝난다.

  1. validators에 등록되어 있는 모든 함수를 실행하면서 유효성검사를 실시한다.
  2. 만약 유효성 검사의 값이 false면 에러를 던진다.
  3. 모든 등록된 유효성검사가 완료되면 원래의 setName()을 실행한다.

모든 데커레이터와 원래 메서드 모두 런타임에 실행되는거다.데커레이터는 함수의 선언 또는 내부 동작만 변경해놓을 뿐이다. 동일한 기능을 하는 코드를 하드코딩해도 되지만 데커레이터를 활용하면 가독성과 유지보수성이 확연히 증가한다. 그래서 데커레이터는 Syntatic Sugar라고 할 수 있다. 런타임에는 tsc에서 컴파일된 결과가 통째로 실행 된다.

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) {
    // validator 객체에 있는 모든 키를 순회하면서 validation로직을 수행한다.
    Object.keys(target.validators).forEach((key) => {
      if (!target.validators[key](args)) {
        // 여기서 유효성 검사를 실행한다.
        throw new BadRequestException(); // 실패하면 오류를 던진다.
      }
    });
    // validation이 끝나면 원래의 메서드를 실행시킨다.
    method.apply(this, args);
  };
}

class User {
  private name: string;

  // 데커레이터는 역순으로 실행되므로 @MinLength가 먼저 실행되고, 그걸 평가하는
  // @Validate가 실행된다. 오류발생시 Validate에서 오류를 발생시킨다.
  @Validate
  setName(@MinLength(3) name: string) {
    this.name = name;
  }
}

const t = new User();
t.setName('Dexter');
console.log('---------');
t.setName('De');

Nest.js와 데커레이터 - 심플한 유저 관리 API

https://github.com/atoye1/Nest.js-Basic-User-service

레퍼런스

한용재, NestJS로 배우는 백엔드 프로그래밍

profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글