NestJS 커스텀 데코레이터 만들기: 로그(@LogTest) 기능 구현 예제


Nest.js도 다른 프레임워크와 마찬가지로 데코레이터를 제공합니다. 

그런데 그중에서도 데코레이터를 커스텀, 즉 내가 사용하기 알맞게 설정이 가능한 '커스텀 데코레이터' 기능을 제공합니다.

이런 커스텀 데코레이터를 알아보기전에 데코레이터가 무슨 동작을 하는지 한번 보겠습니다.

※ 정의

데코레이터는 이름 그대로 "장식해 주는 녀석"입니다. 클래스, 메서드, 프로퍼티 위에 붙어서 기능을 덧붙이거나 수정하는 역할을 합니다.중요한 사실은, 데코레이터는 마법이 아니라 그냥 함수(Function)라는 점입니다.
정확
히는 "다른 함수를 감싸서 기능을 확장시키는 고차 함수(Higher-Order Function)"입니다.

●  직접 해보자

코드는 말로하는것보다 한번 짜보는게 제일 수월합니다. 아래와같이 메서드가 실행될때 자동으로 시작과 종료 로그를 찍어주는 데코레이터 예제를 통해서 데코레이터를 만들고 테스트 해보겠습니다.

< 코드 >

log-test.decorator.ts
/**
 * 커스텀 데코레이터: LogTest
 * 메서드가 실행될 때 "실행 시작"과 "종료" 로그를 남깁니다.
 */
export function LogTest(message: string = '') {
  // 1. 데코레이터 팩토리: 데코레이터에 인자를 넘겨주기 위한 함수
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    // 2. 원래의 메서드를 따로 저장해둡니다. (나중에 실행해야 하니까요!)
    const originalMethod = descriptor.value;

    // 3. 원래 메서드를 우리의 '새로운 함수'로 덮어씁니다.
    descriptor.value = function (...args: any[]) {
      // [실행 전] 원하는 동작 수행
      console.log(
        `[LogTest] ${propertyKey} 메서드 실행 시작! ${message ? `- 메시지: ${message}` : ''}`,
      );

      // 4. 원래 메서드를 실행 (this와 인자들을 그대로 전달)
      const result = originalMethod.apply(this, args);

      // [실행 후] 원하는 동작 수행
      console.log(`[LogTest] ${propertyKey} 메서드 실행 종료.`);

      // 원래 메서드의 결과를 반환
      return result;
    };

    return descriptor;
  };
}
app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { LogTest } from './common/decorators/log-test.decorator';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @LogTest('블로그 포스팅용 예제입니다!')
  getHello(): string {
    return this.appService.getHello();
  }
}
app.Module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
app.service.ts
import {
  Injectable,
} from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
app.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});

 

● 출력 결과

기대되는 출력결과는 메서드가 실행됨에 따라 시작과,종료 로그가 출력되는 것인데 한번 확인해보겠습니다.

우선 위에서 작성한 app.controller.spect.ts 테스트코드가 실행되게 변경을 해주고 실행을 하면 결과는 아래와 같습니다.

출력

자 그럼 위처럼 출력되기까지 보면 데코레이터 함수는 컨트롤러의 상단에 @로 첨부하였고 해당엔드포인트가 호출되니 바로 아래의 펑션이 실행되었습니다.

데코레이터 펑션

여기서 설명하자면  아래와 같습니다.

반응형

코드 설명

1. export function LogTest(...)
 * 데코레이터는 본질적으로 '함수'입니다.
 * 우리가 @LogTest()라고 괄호를 열고 닫으며 호출하는 것처럼 LogTest라는 함수가 실행되어야 한다는 뜻입니다.

 2. return function (target, propertyKey, descriptor)
 * 이게 바로 '데코레이터 함수'의 본체입니다. 
 * LogTest()가 실행되면, 이 내부 함수가 리턴되어 NestJS(또는 TS)에게 전달됩니다.  
 * - target: 이 데코레이터가 붙은 클래스 (AppController)
 * - propertyKey: 메서드 이름 ("getHello")
 * - descriptor: 메서드의 속성 설명서 (여기에 진짜 함수가 들어있음)

 3. 원래 있던 진짜 메서드(getHello)를 변수에 백업해 둡니다.
 *나중에 우리가 원할 때 실행시켜주기 위함입니다.

4. descriptor.value = function (...)
 * '메서드 바꿔치기(Hijacking)을 하는 부분
 * descriptor.value는 원래 getHello 함수가 들어있던 '변수(공간)'입니다.
 * 여기에 우리가 만든 '새로운 가짜 함수'를 대입(=)합니다.
 * 이제 getHello()를 부르면, 원래 함수 대신 이 '함수'가 실행됩니다

5. originalMethod.apply(this, args)
  * 백업해뒀던 '진짜 함수'를 여기서 실행합니다.  
  * - this: 아주 중요! AppController 인스턴스를 그대로 넘겨줍니다.
  * - args: 파라미터들도 그대로 토스합니다.

6. 원래 함수의 결과값(return "Hello World")을 돌려줍니다.


Q1. 왜 class 안의 메서드처럼 정의하지 않나요?

A. 데코레이터는 클래스가 아니기 때문입니다.데코레이터는 독립적인 함수 파일입니다. 자바스크립트 문법상 변수(descriptor.value)에 함수를 할당할 때는 function 표현식을 써야 합니다.만약 클래스와 메서드 문법을 써서 체계적으로 관리하고 싶다면, NestJS의 Interceptor(인터셉터) 기능을 사용하는 것이 맞습니다.

또한 class 처럼 사용하려면 아래처럼 로직을 구성해야하여 function으로 사용합니다.

class MyDecorators {
    LogTest() { ... } // 메서드
  }
 
  // 사용하려면 이렇게 써야 합니다.
  @new MyDecorators().LogTest()

Q2. async/await도 붙이나요?

 대부분 비동기니까. 붙이면 좋습니다. 컨트롤러의 메서드가 DB에서 작업하느라 Promise를 반환할 수 있습니다. 이때 await 없이 그냥 호출하면, "종료" 로그가 작업이 끝나기도 전에 먼저 찍혀버립니다. 비동기 메서드까지 완벽하게 지원하려면 데코레이터 내부를 async 으로 만들어주는 것이 좋습니다.


이를 통해서 알수있는 사실은 데코레이터 또한 하이재킹처럼 함수를 가로채는 기술과 같다는 것을 알수 있습니다.
이사실 만으로도 인증과 유효성검사 로깅 등 반복되는 로직 등을 데코레이터로 따로 분리해 낼수 있습니다.

 

 

반응형