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)}`);
...
아래처럼 로그가 잘 찍힌다
