본문 바로가기
카테고리 없음

Nestjs 커스텀 로그 decorator

by bieber085 2022. 9. 15.

Nestjs 리팩토링 진행 중에 로그를 어노테이션으로 처리하고 싶어서 구현한 내용을 적어보았다

 

먼저 Nestjs의 LoggerService interface를 구현해서 커스텀 로거 서비스를 만든다.

간단히 앞에 로그레벨을 붙여서 메시지를 출력하는 로거를 만들었다.

import { Injectable, LoggerService } from '@nestjs/common';

@Injectable()
export class MyLogger implements LoggerService {
  log(message: any, ...optionalParams: any[]) {
    console.log(`[log] ${message}`);
  }
  error(message: any, ...optionalParams: any[]) {
    console.error(`[error] ${message}`)
  }
  warn(message: any, ...optionalParams: any[]) {
    console.error(`[warn] ${message}`)
  }
  debug?(message: any, ...optionalParams: any[]) {
    console.log(`[debug] ${message}`)
  }
  verbose?(message: any, ...optionalParams: any[]) {
    console.log(`[verbose] ${message}`)
  }
}

이전에는 아래처럼 로거 모듈을 만들어 app 모듈 imports에 추가시키고

다른 서비스에서 DI를 통해 로거 서비스에 접근하여 사용했다.

import { Module } from '@nestjs/common';
import { MyLogger } from './logger.service';

@Module({
  providers: [MyLogger],
  exports:  [MyLogger]
})
export class LoggerModule {}
@Resolver(of => User)
export class UsersResolver {
    constructor(
        private readonly usersService: UsersService,
        private readonly logger: MyLogger, //MyLogger DI
    ) { }
    
    @Mutation(returns => LoginOutput)
    async login(
        @Args('input') loginInput: LoginInput,
    ) {
        this.logger.log('User has logged in'); // MyLogger를 통해 로깅
        return this.usersService.login(loginInput); 
    }
}

 

하지만 모든 서비스에서 일일이 로거를 임포트하는 번거로움이 있고 일반적으로 arguments와 return 값 로깅만 할 예정이라 같은 코드를 반복하게 되어 데코레이터로 처리하는 방법을 택했다.

 

Method 실행 전에 arguments를 프린트하는 간단한 Logger 데코레이터를 만들었다.

기존의 함수를 args를 프린트 한 후에 원래 함수를 호출하고 리턴하는 새 함수로 대체해주는 데코레이터이다.

export function Logger() {

  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log(`args:${JSON.stringify(args)}`);
      try {
        return method(...args);
      } catch (e) {
        console.log(e);
      }
    }
  }
}

 

 

이제 로깅하고 싶은 메서드에 데코레이터를 사용하면 변수들이 출력된다.

export class UsersResolver {
    //...
    @Mutation(returns => LoginOutput)
    @ArgsLogger()
    async login(
        @Args('input') loginInput: LoginInput,
    ) {
        return this.usersService.login(loginInput); 
    }
}

 

하지만 함수를 실행해보면 아래처럼 에러로그가 발생한다. 

args는 정상적으로 프린트가 되었지만 위 login함수 실행시 usersService가 undefined라는 에러가 나타난다.

this.usersService가 undefined인 이유는 Logger 데코레이터를 사용하면서 실행 컨텍스트가 변경되었기 때문이다. 

데코레이터 사용 전의 this는  userResolver 오브젝트이지만, 데코레이터를 사용하면서 대체된 함수기 실행될 때 this는 undefined가 된다. 따라서 this를 바인딩해줘야 하는데 함수마다 바인딩 해야할 오브젝트가 다르므로 구현하기 쉽지 않다.

 

그래서 대신 프록시 apply 핸들러로 로그 출력을 하는 식으로 구현했다.

export function ArgsLogger() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value; //원래 메서드
    descriptor.value = new Proxy(method, {
      //apply 핸들러로 입력 변수와 리턴 값 출력
      apply: function (target, thisArg, args) {
        console.log(`${propertyKey} : ${JSON.stringify(args)}`);
        try {
          const result = target.apply(thisArg, args);
          console.log(`${propertyKey} => ${result}`);
          return result;
        } catch (error) {
          console.error(`${propertyKey} => ${JSON.stringify(error)}`);
          throw error;
        }
      },
    })
  };
}

 

콘솔 로그 대신에 따로 작성한 로거 서비스를 사용하고 싶다면 Logger 모듈을 global로 설정하고 데코레이터에 의존성을 주입하면 된다.

export function ArgsLogger() {
  const injectLogger = Inject(MyLogger); //Inject 추가
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    injectLogger(target, 'logger'); //target에 logger라는 키로 inject함
    const method = descriptor.value;
    ...
    thisArg.logger.log(`${propertyKey} : ${JSON.stringify(args)}`);
    ...

 

아래처럼 로그가 잘 찍힌다