카테고리 없음

Nestjs 커스텀 로그 decorator

bieber085 2022. 9. 15. 15:47

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)}`);
    ...

 

아래처럼 로그가 잘 찍힌다