Nestjs - Redis Dynamic Module

atesi·2023년 5월 22일
0

이번 포스팅은 지난 포스팅에서 다루었던 레디스 클러스터 모듈을 이번에는 직접 동적 모듈로 생성해보고 이 과정에서 알게된 내용을 정리해보았습니다.

Module

모듈은 @Module() 데코레이터가 적용된 클래스입니다. @Module() 데코레이터는 Nest가 애플리케이션 구조를 구성하는데 사용하는 메타데이터를 제공합니다.

Nestjs에서 모듈은 어플리케이션에 주입하기 위해 특정한 기준으로 캡슐화된 코드 집합입니다. 모듈로 애플리케이션을 분할함으로써 보다 구조화되고 관리 가능한 방식으로 구성할 수 있습니다. 각 모듈은 고유의 controller, provider 및 기타 관련 컴포넌트를 가질 수 있습니다.

예를 들어 사용자 controller, 사용자 service, 사용자 repository 및 기타 관련 Provider를 포함한 사용자 관련 기능을 처리하기 위한 모듈이 있습니다. 이 모듈은 사용자 등록, 인증 등과 같은 사용자와 관련된 모든 작업을 처리합니다.

마찬가지로 제품 관련 기능을 처리하기 위한 또 다른 모듈이 있을 수 있습니다. 이 모듈은 제품 생성, 업데이트 및 검색과 같은 작업을 처리합니다.

이렇게 서로 다른 모듈로 분리하여 책임을 명확히 하고 코드베이스를 보다 체계화하고 유지 관리하기 쉽게 만들 수 있습니다. 또한 모듈 내에서 관련 기능을 캡슐화함으로써 해당 모듈을 애플리케이션의 다른 부분이나 다른 프로젝트에서 쉽게 재사용할 수 있습니다.

configuration

@Module({
	imports: [StudyModule],
	controllers: [SubController, AppController, StudyController],
	providers: [AppService, StudyService, ChildService, TestServiceA],
    exports: []
})
export class AppModule {}

Decorator: 모듈은 @nestjs/common 패키지에서 가져온 @Module() 데코레이터를 사용하여 정의됩니다. 이 데코레이터는 import, controllers, providers 및 exports와 같은 다양한 구성 옵션이 있는 객체를 받아옵니다.

Imports: imports 속성은 현재 모듈 내에서 가져와 사용할 수 있는 다른 모듈의 배열을 명시합니다. 이를 통해 모듈 간의 종속성을 설정하여 가져온 모듈의 컴포넌트, providers 및 controllers를 사용할 수 있습니다.

Controllers: controllers 속성 현재 모듈에 속하는 컨트롤러의 배열을 정의합니다. 컨트롤러는 들어오는 HTTP 요청을 처리하고 해당 요청을 처리하는 라우트 핸들러 (예: @Get(), @Post()와 같은 데코레이터)를 포함합니다.

Providers: porviders 속성은 현재 모듈로 범위가 지정된 프로바이더 배열을 지정합니다. 프로바이더는 데이터 액세스, 비즈니스 로직, 서비스 등과 같은 다양한 작업을 처리합니다. 모듈 내의 컨트롤러나 다른 프로바이더에 주입될 수 있습니다.

Exports: exports 속성은 다른 모듈에 특정 모듈, 컴포넌트 또는 프로바이더를 사용할 수 있도록 합니다. 이 기능을 사용하여 애플리케이션의 다른 부분 또는 외부 모듈에서 재사용할 수 있습니다.

Static and Dynamic Modules

NestJS에서 일반적으로 사용되는 두 가지 유형의 모듈은 정적 모듈과 동적 모듈입니다.

static modules

정적 모듈은 NestJS의 전통적인 접근 방식으로, 모듈은 고정 구성 및 종속성으로 컴파일 타임에 정의됩니다. @Module() 데코레이터로 클래스를 데코레이션하면 정적 모듈이 생성됩니다.

컴파일 타임에 정의된다는 것은 애플리케이션의 구조와 종속성이 컴파일 프로세스 중에 결정되고 수정되며, 런타임에 변경되지 않는다는 것을 의미합니다. 모듈이나 구성을 변경하려면 애플리케이션을 다시 컴파일해야 합니다.

dynamic modules

NestJS에서 동적 모듈은 런타임에 동적으로 모듈을 만드는 방법이다. 런타임 조건이나 외부 구성을 기반으로 동적 구성, 종속성 및 공급자가 있는 모듈을 생성할 수 있습니다.

동적 모듈은 다양한 구성의 모듈을 만들어야 하거나 런타임에 모듈의 종속성을 결정해야 할 때 필요합니다. 기본 NestJS 모듈은 정적이며 구성되는 방식에 영향을 줄 수 없습니다.

시작하기 앞서 동적 모듈의 의미를 알아봅시다. 각 모듈이 데이터를 처리하는 행동방식을 바꾸기 위한 외부 API에서 파라미터를 받아 비정적 모듈을 만드는것을 중심으로 합니다.

예를 들어, 데이터베이스에서 데이터를 쿼리하기 위한 모듈을 만들지만, 특정 데이터베이스 공급자를 하드코딩하고 싶지 않다고 가정해 봅시다. 이 문제를 어떻게 해결할까요?

먼저, 구성 함수를 갖는 모듈을 생성해야 합니다. 구성 함수는 데이터베이스 공급자 인터페이스를 매개변수로 사용하며, 이 인터페이스는 애플리케이션이 데이터베이스에 연결하고 쿼리하는 데 필요한 모든 필수 함수를 포함합니다. 인터페이스를 매개변수로 사용하기 때문에 공급자가 해당 인터페이스를 확장하는 한 다른 데이터베이스 공급자를 주입할 수 있습니다.

데이터베이스 공급자는 초기화 시 제공한 공급자에 따라 변경됩니다. 따라서 모듈은 더 이상 정적이지 않고 동적인 것이 됩니다.

Use Cases

먼저 완성된 코드를 보고 천천히 정리해보겠습니다.

//redis.module.ts

import { DynamicModule, FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { Module } from '@nestjs/common';
import IORedis, { ClusterOptions, Cluster } from 'ioredis';
import { ClusterNodes } from './type';

export const IORedisKey = 'IORedis';

type ClusterModuleOptions = {
  nodes: ClusterNodes[];
  connectionOptions: ClusterOptions;
  onClientReady?: (client: Cluster) => void;
};

type ClusterAsyncModuleOptions = {
  useFactory: (
    ...args: any[]
  ) => Promise<ClusterModuleOptions> | ClusterModuleOptions;
} & Pick<ModuleMetadata, 'imports'> &
  Pick<FactoryProvider, 'inject'>;

@Module({})
export class ClusterModule {
  static async registerAsync({
    useFactory,
    imports,
    inject,
  }: ClusterAsyncModuleOptions): Promise<DynamicModule> {
    const redisProvider = {
      provide: IORedisKey,
      useFactory: async (...args) => {
        const { nodes, connectionOptions, onClientReady } = await useFactory(
          ...args,
        );

        const client = new IORedis.Cluster(nodes, connectionOptions);

        onClientReady(client);

        return client;
      },
      inject,
    };

    return {
      module: ClusterModule,
      imports,
      providers: [redisProvider],
      exports: [redisProvider],
    };
  }
}
//module.config.ts

import { Logger } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClusterModule } from './redis.module';

export const redisModule = ClusterModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => {
    const logger = new Logger('RedisClusterModule');

    return {
      nodes: [
        { host: '127.0.0.1', port: 7001 },
        { host: '127.0.0.1', port: 7002 },
        { host: '127.0.0.1', port: 7003 },
      ],
      connectionOptions: {
        scaleReads: 'slave',
        natMap: {
          '173.17.0.2:7001': {
            host: '127.0.0.1',
            port: 7001,
          },
          .
          .
          .
          '173.17.0.7:7006': {
            host: '127.0.0.1',
            port: 7006,
          },
        },
      },

      
      onClientReady: (client) => {
        logger.log(`Redis client ${client.status}`);

        client.on('error', (err) => {
          logger.error('Redis Client Error: ', err);
        });

        client.on('connect', () => {
          logger.log(`Connected to redis`);
        });
      },
    };
  },
  inject: [ConfigService],
});

part 1

이제 Redis Cluster Dynamic Module을 만들어 보겠습니다. 먼저 내용을 채워넣기 전에 기본적인 아웃라인을 확인해 봅니다.

@Module({})
export class ClusterModule {
  static async registerAsync(): Promise<DynamicModule> {
    return {
      module: ClusterModule, 
      imports: [],
      providers: [],
      exports: [],
    };
  }
}

우선 @Module을 데코레이터를 사용해 모듈을 정의하고 Promise<DynamicModule>을 반환하는 registerAsync를 정의 해줍니다. DynamicModule은 모듈에 충족하는 필드를 포함한 객체입니다.

이제 registerAsync에 들어갈 매개변수를 설정해 줄겁니다.

 type ClusterModuleOptions = {
  nodes: ClusterNodes[]; 
  connectionOptions: ClusterOptions; 
  onClientReady?: (client: Cluster) => void;
};

ClusterModuleOptionsClusterNodes[]connectionOptions를 포함하는 레디스 클러스터의 구성 옵션을 정의해줍니다.

export interface ClusterNodes{
    host:string;
    port:number;
  }

ClusterstartupNodes를 설정해주기 위해 type.ts를 따로 만들어주고 ClusterNodes 인터페이스를 만들어 주었습니다. 클러스터로 구성했기 때문에 배열로 받아옵니다.

connectionOptions: ClusterOptions 클러스터 옵션인 natMap, scaleReads 등을 설정 하기 위해 ioredisClusterOptions로 지정해줍니다.

onClientReady?: (client: Cluster) => void: 선택적으로 클라이언트 (Cluster 인스턴스)가 준비되었을 때 호출되는 콜백 함수를 만들어줍니다.

type ClusterAsyncModuleOptions = {
  useFactory: (
    ...args: any[]
  ) => Promise<ClusterModuleOptions> | ClusterModuleOptions;
} & Pick<ModuleMetadata, 'imports'> &
  Pick<FactoryProvider, 'inject'>;

ClusterAsyncModuleOptions는 추가적인 속성을 포함한 ClusterModuleOptions의 확장된 버전입니다. 이 속성에는 useFactory가 있으며 이 속성은 가변인자 ...args: any[]를 받고 Promise<ClusterModuleOptions> 또는 ClusterModuleOptions를 직접 반환하는 함수입니다.

imports: ModuleMetadata 유형에서 선택된 속성입니다. 이 비동기 모듈이 의존하는 모듈 목록을 지정할 수 있게 합니다.

inject: FactoryProvider 유형에서 선택된 속성입니다. inject를 포함함으로써, useFactory가 요구하는 종속성을 지정하고 팩토리 함수를 호출할 때 NestJS가 주입하도록 할 수 있습니다.

@Module({})
export class ClusterModule {
  static async registerAsync({
    useFactory,
    imports,
    inject,
  }: RedisAsyncModuleOptions): Promise<DynamicModule> {
    return {
      module: ClusterModule, 
      imports: imports,
      providers: [],
      exports: [],
    };
  }
}

이제 registerAsync에 설정해준 매개변수를 넣어 줍니다.

part 2

const redisProvider = {
  provide: 'IORedisKey',
  useFactory: async (...args) => {
    const { nodes, connectionOptions, onClientReady } = await useFactory(
      ...args,
    );

    const client = new IORedis.Cluster(nodes, connectionOptions);
    onClientReady(client);

    return client;
  },
  inject,
};

registerAsync 안에 redisProvider를 생성해 줍니다. 동적으로 프로바이더를 만들기 위해 Factory Provider를 사용합니다. NestJS에서 provider를 생성할 때는 해당 provider에 대한 provide 속성을 지정해야 합니다. 이 속성은 provide token으로 사용되며, provider를 고유하게 식별하는 역할을 합니다. 위에서 provide 속성을IORedisKey로 설정해 주었습니다.

constructor(
    @Inject(IORedisKey) private readonly redisClient: Cluster,
  ) {
    this.ttl = configService.get('CHAT_DURATION');
  }

위와 같이 사용 가능합니다. 다음 포스트에서 실제로 모듈을 이용해서 동작을 확인할 예정입니다.

다음은 useFactory입니다. 가변인자를 받아 client를 내보냅니다.

구조 분해 할당을 사용하여 resolved된 ClusterModuleOptions 객체에서 nodes, connectionOptions, onClientReady 속성을 추출합니다.

IORedis.Cluster의 새로운 인스턴스를 생성합니다. nodesconnectionOptions를 전달하여 클라이언트를 구성합니다.

onClientReady 함수가 있으면 생성된 Redis 클라이언트를 인수로하여 호출합니다. 아래 설정에서 간단한 로거를 작성할 것 입니다.

클라이언트를 반환합니다.

part 3

import { Logger } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClusterModule } from './redis.module';

export const redisModule = ClusterModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => {
    const logger = new Logger('RedisClusterModule');

    return {
      nodes: [
        { host: '127.0.0.1', port: 7001 },
        // { 
        //   host: configService.get('REDIS_HOST'),
        //   port: configService.get('REDIS_PORT') 
        // },
        { host: '127.0.0.1', port: 7002 },
        { host: '127.0.0.1', port: 7003 },
      ],
      connectionOptions: {
        scaleReads: 'slave',
        natMap: {
          '173.17.0.2:7001': {
            host: '127.0.0.1',
            port: 7001,
          },
          .
          .
          .
          '173.17.0.7:7006': {
            host: '127.0.0.1',
            port: 7006,
          },
        },
      },

      
      onClientReady: (client) => {
        logger.log(`Redis Cluster client ${client.status}`);

        client.on('error', (err) => {
          logger.error('Redis Cluster Client Error: ', err);
        });

        client.on('connect', () => {
          logger.log(`Connected to Redis Cluster`);
        });
      },
    };
  },
  inject: [ConfigService],
});

내보낼 redismoduleClusterModule.registerAsync를 이용하여 정의해줍니다. 구성한 redisCluster 설정에 맞게 입력해 줍니다. .env에 미리 설정해둔 변수들이 있다면 이용해줍니다.

모든 작업을 완료했습니다. 모듈을 등록하고 서버를 실행해 로그를 확인해 봅니다.

참고
https://docs.nestjs.com/modules
https://blog.logrocket.com/use-configurable-module-builders-nest-js-v9/#what-nest-js-modules
https://www.nokiahub.site/dev/nestjs-basic/
https://velog.io/@from_numpy/Nest-%EC%A0%95%EC%A0%81-%EB%AA%A8%EB%93%88-%EB%B0%94%EC%9D%B8%EB%94%A9-vs-%EB%8F%99%EC%A0%81-%EB%AA%A8%EB%93%88-%EB%B0%94%EC%9D%B8%EB%94%A9
https://github.com/JacobSNGoodwin/ranker-course/blob/e6654583fbdf9022485d01336e4ee9a56985297c/tutorials/04-create-redis-module.md

profile
Action!

0개의 댓글