[고스락 티켓 2.0] nestjs redis forRootAsync 모듈 만들기
위 사진처럼 useFactory를 사용해서 configSerivce를 넘겨줘서 모듈을 세팅하는 경우가있다.
환경변수들을 configService를 이용해서 의존성 주입시키는게 joi 통해서도 밸리데이션 거친 값이기도하니,
모듈설정할 때 많이 쓰는 패턴이다.
고스락 티켓 예매 프로젝트 ( 이하 고티켓 ) 에서는 redis를 인증번호 3분 ttl 저장이나 , bull js 큐로 사용중에 있는데
특히 인증번호 저장하고 꺼내오는 로직이 필요해서 redis 를 관리하는 모듈을 만들게되었는데
하다보니 다른 프로젝트에서도 쓸일이 있을것같아 모듈을 임포트할때 forRootAsync 처럼 configSerivce 나 다른 설정값들을 줘서 동적으로 설정할 수있는 모듈을 만들어 볼려고 한다.
만들려고 진짜 많이 찾아봤는데 딱히 없는것같아서
이런 nest에서 제공해주는 프로젝트 소스 보면서 작성을 했다.
사실 저 forRootAsync 라는 메서드의 이름도 그냥 컨벤션 같다. 딱 정해진 이름이나 규칙없이 forRoot, forRootAsync 뭐 이런식으로 명명하는것같다.
우리가 최종적으로 만들 형태는 다음과 같다.
이글에서 얻어갈 수 있는것들
- forRootAsync DynamicModule 직접 만들기
- 모듈 내부의 useFactory inject 이해하기
- useFactory 를 사용해서 모듈 내부 로깅을 껏다 키기 with FakeLogger
이글에 소스들은 고스락 티켓 예매 프로젝트에서 사용하고 있는 소스들입니다.!
우선 요구사항을 한번 정리하고 가보자. 필요한 기능은
- configSerivce를 주입해서 환경변수를 받아 다이나믹하게 모듈을 설정할 수 있어야한다.
- 레디스 서비스를 통해서 저장이나 , 조회를 할 수있어야한다. ( 커넥션된 레디스 인스턴스가 있어야함)
- logging 설정값을통해서 로그를 껏다 킬 수 잇어야한다
이러한 요구조건들이있고
우리는 forRootAsync static메서드를 통해서 동적으로 모듈을 임포트 할수 있는 코드를 작성할 것이다.
DynamicModule
우리가 생각하는 일반적인 모듈의 모습은 위와같다 . 저렇게 하면 UserModule 클래스의 모듈 하나만 임포트를 할 수 가있다.
하지만 우린 configService를 주입해서 동적으로 모듈을 가져와야 할 때도 있고. 아니면 다른 메서드를 통해 다양한 방법으로 모듈을 가져오고 싶을 때가 있다. 그래서 nestjs 는 모듈 클래스의 스태틱 메서드를 활용해서 DynamicModule 타입을 반환을 하면,
여러 메서드를 통해서 다양한 방법으로 모듈을 임포트 할 수있는 방법을 지원 할 수있는것이다.
forRoot -> 그냥 생으로 집어넣는느낌 내 서비스를 저 모듈에 주입을 시켜서 값을 뽑아내올 수가없다.
forRootAsync -> configSerivce 같은걸 주입할 수 있따!!! useFactory등 사용가능!
오늘우리는 forRootAsync 를 제작할거고 사실 forRoot 같은건 쉽다
아래 소스는 naver sms 문자 메시지 발송때 forRoot 형태로 쓰는법 , forRootAsync 형태로 쓰는법 두가지를 기술해놨다.
사실 내가 막 npm 패키지 만들어서 할려면 forRootAsync 형태로 작성하는게 맞고, 내부적으로만 쓸꺼야하면 forRoot 로 쓰는거다.
오늘 우리는 레디스모듈forRootAsync 만 볼거다.
forRootAsync DynamicModule 직접 만들기
forRootAsync 라는 스태딕 메서드를 만들고, 이 스태틱 메서드는 DynamicModule을 리턴한다.
그러면 일반적인 모듈을 설정할때 import 구문에 DynamicModule 타입을 집어넣을 수가있다.
여기서 중요한점은 인자로 넘겨줄때 RedisAsyncConfig 타입으로 넘겨주는데
forRootAsync 스태틱 메서드를 통해서 안에서 필요한 로직들을 돌리고 DynamicModule를 리턴한다는 점이다
우선적으로 RedisAsyncConfig 에대해서 알아보자
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { RedisOption } from './RedisOption.interface';
export interface RedisAsyncConfig extends Pick<ModuleMetadata, 'imports'> {
/**
* Factory function that returns an instance of the provider to be injected.
*/
useFactory: (...args: any[]) => Promise<RedisOption>;
/**
* Optional list of providers to be injected into the context of the Factory function.
*/
inject: FactoryProvider['inject'];
}
src/redis/config/RedisAysncConfig.interface.ts
중요한 뽀인트! 는 inject를 적어주어야지만 useFactory 내부에서 서비스를 주입해서 사용 할수 있다는점인데 useFactory를 어떠한방식이든 실행시켜서 우린 RedisOption이라는 타입의 결과값을 얻을 것이고.
RedisOption 결과값이 나오면 우린 그 결과를 가지고 레디스 커넥션이나 로깅 설정등을 할것이다.
즉 현재 RedisAsyncConfig 에선 내가 커스텀으로 만들 모듈내부에서 동적으로 실행시킬 부분을 받으면서 useFactory에 inject을 해줘야할 서비스도 가져와서 실행시켜서 토큰 기반으로 내 모듈 내부에 값들을 공급 해준다고 보면된다.
모듈 provider에 토큰기반으로 모듈내에 다른 프로바이더가 해당 주입된 값이나 인스턴스등을 사용할 수 있도록 공유를 해주는 것이다.
뒤에서 다시 언급할 테니 일단 넘어가고
일단 useFactory의 리턴값인 RedisOption 부분에 대해서 보도록 하자.
import { RedisClientOptions } from 'redis';
export interface RedisOption {
// 실제 레디스에 연결하지 않고 테스트를 원하면
isTest: boolean;
// 로깅을 원하면 true로
logging: boolean;
redisConnectOption: RedisClientOptions;
}
src/redis/config/RedisOption.interface.ts
레디스 커넥션 옵션은 뭐... 실제 커넥션 옵션이고
isTest는 테스트 즉 실제 레디스에 연결하지 않고 테스팅을 원하면 redisTest.service.ts 를 주입하는 용도고
logging 은 로깅을 껏다 킬지 에대한 부분이다. ( 로깅을 끌때는 소스를빼는게아니라 가짜 로거를 주입시켜 아무것도 동작안하는 메소드를 실행시키는 거다. )
이렇듯 useFactory와 inject를 모듈 설정에서 받아서 뭐 어떻게 돌리면? RedisOption이라는 걸 반환해서 Redis 모듈내부에서 그 값을 사용할 수 있는 타입에 대해서만 보았다. 이제 redis 모듈을 들여다 보도록 하자.
모듈 내부의 useFactory inject 이해하기
import { DynamicModule, Global, Logger, Module } from '@nestjs/common';
import { createClient } from 'redis';
import { FakeLogger } from './FakeLogger';
import { RedisService } from './redis.service';
import { RedisAsyncConfig } from './config/RedisAsyncConfig.interface';
import { RedisOption } from './config/RedisOption.interface';
import {
REDIS_CLIENT_PROVIDER,
REDIS_MODULE_OPTIONS
} from './config/Redis.const';
// 레디스 모듈에서 클라이언트 타입 지원안해줘서 뽑아줬음 나쁜넘들.
export type RedisClientType = ReturnType<typeof createClient>;
@Global()
@Module({})
export class RedisModule {
static forRootAsync(redisAsyncConfig: RedisAsyncConfig): DynamicModule {
return {
module: RedisModule,
providers: [
{
provide: REDIS_MODULE_OPTIONS,
useFactory: redisAsyncConfig.useFactory,
inject: redisAsyncConfig.inject || []
},
{
provide: REDIS_CLIENT_PROVIDER,
useFactory: async (options: RedisOption) => {
const client = createClient(options.redisConnectOption);
client.on('error', err => console.log('Redis Client Error', err));
await client.connect();
return client;
},
inject: [REDIS_MODULE_OPTIONS]
},
{
provide: Logger,
useFactory: (options: RedisOption) => {
return options.logging
? new Logger('RedisService')
: new FakeLogger();
},
inject: [REDIS_MODULE_OPTIONS]
},
RedisService
],
exports: [RedisService]
};
}
}
// redis/config/Redis.const.ts
export const REDIS_CLIENT_PROVIDER = 'REDIS_CLIENT_PROVIDER';
export const REDIS_MODULE_OPTIONS = 'REDIS_MODULE_OPTIONS';
src/redis/redis.module.ts
다시, RedisModule 내부에 forRootAsync 스태딕 메소드를 통해,
위에서 우리가 RedisAsyncConfig 타입으로 인자를 받았던 redisAsyncConfig 인자를 가져와서
뭘 어떻게든 돌려서 DynamicModule을 만들어서 반환하고 있다.
프로바이더의 제일 처음부분을 보자.
provide 부분은 어차피 토큰 기반으로 작동을한다 클래스 타입을 적으면 class.name 찍어서 스트링 형태의 토큰을 가져간다.
우리는 useFactory 를 돌리면 나오는 RedisOption 타입인 객체를 내가 커스텀으로 만들 모듈에 공급을 해줄건데
그 이름은 REDIS_MODULE_OPTIONS 라고 정하는것이고
우리는 useFactory 를 돌리면서 redisAsyncConfig 에 인자로 적어줬던 inject를 적으면서 useFactory에 의존성 주입을 해줄것이다.
inject 설명 : Optional list of providers to be injected into the context of the Factory function.
이렇게 모듈을 임포트할때 적어줬던 값들이 레디스 모듈의 스태틱 메소드로 실행되면서 모듈 내부에 토큰기반으로 값들이 전파되는 느낌으로 흘러간다.
그다음 우리는 REDIS_MODULE_OPTIONS 토큰을 통해서 RedisOption 타입인 객체를 가져온뒤에
useFactory를 또사용해서 inject로 RedisOption 타입인 객체를 공급해준다.
이제 우린 redis와 커넥션을 맺은 인스턴스를 REDIS_CLIENT_PROVIDER 토큰으로 공급을 해줄 것이다.
구럼 이거 어떠써먹어용?
redisSerivce
import { Inject, Injectable, Logger, LoggerService } from '@nestjs/common';
import { RedisClientType } from '@redis/client';
import { REDIS_CLIENT_PROVIDER } from './config/Redis.const';
@Injectable()
export class RedisService {
constructor(
@Inject(Logger) private logger: LoggerService,
@Inject(REDIS_CLIENT_PROVIDER) private redisClient: RedisClientType
) {
}
async getByKeyValidationNumber(key: string): Promise<string | null> {
this.logger.log(`get ${key}`);
const validationNumber = await this.redisClient.get(key);
return validationNumber;
}
}
srv/redis/redis.service.ts
위에 보이듯 Inject 써서 커넥션된 레디스 클라이언트를 가져와서 쓰는 구조이다! 간단하다 ㅋ
이제 이 레디스 서비스를 다른데서 임포트해와서 쓰면 된당
useFactory 를 사용해서 모듈 내부 로깅을 껏다 키기 with FakeLogger
마지막 부분이다!
di 잘 활용해서 options 객체에는 이제 logging 값들도 꺼낼 수 있으니 가져와서 true false 에 따라 로깅객체를 선택해서 반환하면 된당.
/* eslint-disable @typescript-eslint/no-empty-function */
import { LoggerService } from '@nestjs/common';
/**
* 로그를 끌 시에 아무것도 안하기위한 fake Logger
* redis module 에서 사용
* 2022-07-13 이찬진
*/
export class FakeLogger implements LoggerService {
log(message: any, ...optionalParams: any[]) {}
error(message: any, ...optionalParams: any[]) {}
warn(message: any, ...optionalParams: any[]) {}
debug?(message: any, ...optionalParams: any[]) {}
verbose?(message: any, ...optionalParams: any[]) {}
}
srv/redis/FakeLogger.ts
구냥뭐.. 페이크 로거이다 로거서비스 따와서 아무것도 실행안하는 걸로 했다.
이렇게 고티켓에서 활용하던 레디스 모듈에대해서 알아보았다.
이런 di를 잘활용해서 서비스 오류알림은 개발환경에서 꺼버리는 슬랙 모듈이나 , 실제 운영환경에서만 문자가가는 sms 모듈등
잘 활용해서 만들었다 이게... 의존성 주입이다 최고다.
모든 소스는 아래 레포에서 확인 가능하시다!
도움 되셨으면 좋아요랑 스타 부탁드려요 ㅎㅎ