Nest.js mongodb-memory-server, cache 적용

개요

테스트 코드를 작성을 하다 보면 독립성을 가진 테스트 코드를 작성하는데 많은 고민을 하게 됩니다. 독립성을 가지기 위해서 DB 관련된 작업을 수행하게 되면 다음 테스트 전에 데이터를 삭제하고 다음 테스트를 수행하게 하는 등 많은 방식을 사용하고 있습니다. 

 

하지만 여기서 문제가 되는 것은 의존성을 가진 채로 테스트를 계속 진행을 하게 되면은 문제가 되는 것이 테스트용 디비를 따로 사용을 하게 되지만 다른 테스트에 결국은 영향을 받기도 하고 테스트 속도면에서도 많이 느려지는 문제가 존재하고 있습니다. 

 

그렇다면 이러한 문제를 해결하기 위해서 디비와 캐시를 메모리를 사용하여 처리하는 방식을 사용하게 되었습니다. 이번 글에서 이러한 연결과 방식은 어떻게 되는 것인지 알아보도록 하겠습니다. 

 

Oringin Module Import 방식

현재 사용하는 디비에 따라 바뀌겠지만 `MongoDB`를 사용한다는 가정하에 설명을 하도록 하겠습니다. 기본적으로 테스트 코드를 작성한다는 것은 새로운 환경을 만들어 주어서 테스트를 한다고 볼 수 있겠습니다. 서버 자체를 구동을 하여 검증하는 `통합테스트, E2E테스트` 가 존재하지만 이러한 경우에도 원하는 독립적인 환경을 만들어 주기 위해서 `Mock`을 새로 의존성주입을 하여 검증을 하게 될 것입니다. 아무튼 기존에 `DB`를 연결하는 방식을 보고 다음 `memoryDB`를 연결하는 방식을 보도록 하겠습니다.

 

우선 `Nest.js`는 아래와 같은 구조를 가지고 있다고 보겠습니다. 

- src
  - app.module
  - auth
    - auth.module

 

`app.module`에서는 우선 디비연결을 위한 `URI`를 입력하고 이후 `auth.module`에서 `mongoose` 사용을 위한 `schema`를 작성을 하여야 할 것입니다. 

@Module({
  imports: [
    MongooseModule.forRoot(process.env.MONGO_URI),
    AuthModule,
  ],
  ...
})
export class AppModule {}
@Module({
  imports: [
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
  ],

  controllers: [AuthController],
  providers: [
    AuthService,
  ],
  exports: [AuthService],
})
export class AuthModule {}

 

이후 이러한 환경에 있는 테스트 코드를 작성을 하려고 하면 `@nestjs/testing` 모듈을 사용하여 `craeateTestingModile`을 사용하여 `test`용 모듈을 작성을 하여야 합니다. 해당 코드는 `nest cli` 를 사용하게 되면 기본적으로 생성되는 `spec.ts`에 작성이 되므로 크게 어려움은 없을 듯 합니다. 

 

테스트를 위한 모듈작성과 의존성 주입은 다음과 같은 모습일 것입니다. 

  let service: AuthService;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
      	AppModule,
        AuthModule
      ],
      providers: [AuthService],
    }).compile();

    service = module.get<AuthService>(AuthService);
  });

 

기존에 사용되던 `AppModule`과 `AuthModule` 을 `import` 해줌으로써 기존에 사용한 설정을 이용을 하고 테스트를 진행 할 수 있습니다. 여기서 당연시 되는 문제가 서버에서 돌아가는 디비를 그대로 사용한다는 것일 겁니다.

 

그렇기 때문에 테스트를 위한 새로운 모듈을 작성하고 `memoryDB`를 사용해보도록 하겠습니다. 

 

mongodb-memory-server

`mongodb-memory-server`는 `MongoDB` 인스턴스를 메모리에 생성하는 패키지입니다. 이는 주로 테스트 환경에서 사용되며, 매번 새롭고 깨끗한 데이터베이스 환경을 제공하여 테스트의 일관성과 격리를 유지하게 해줍니다. 

 

특징

  • 빠른 테스트: 메모리 기반으로 실행되므로 일반 `MongoDB` 인스턴스보다 테스트가 빠르게 실행됩니다.
  • 격리된 테스트: 각 테스트 케이스나 테스트 스위트마다 새로운 `MongoDB` 인스턴스를 생성할 수 있으므로 테스트 간의 상태 간섭을 피할 수 있습니다.
  • 편리성: 별도의 `MongoDB` 서버 설치나 설정 없이 쉽게 사용할 수 있습니다.

 

이제 `memoryDB`에 연결 할 수 있게 해주는 `Helper Class`를 작성해주도록 하게습니다. 

// helpers/mongodbHelper.ts

import { MongoMemoryServer } from 'mongodb-memory-server';
import { Connection, connect } from 'mongoose';

export class MongodbHelper {
  private static mongod: MongoMemoryServer;
  private static mongoConnection: Connection;

  static async start(): Promise<void> {
    this.mongod = await MongoMemoryServer.create();
    const uri = this.mongod.getUri();
    this.mongoConnection = (await connect(uri)).connection;
  }

  static async stop(): Promise<void> {
    await this.mongoConnection.dropDatabase();
    await this.mongoConnection.close();
    await this.mongod.stop();
  }

  static getUri(): string {
    return this.mongod.getUri();
  }
}

 

코드를 보게 되면 우선 `memory DB`애 연결을 하고 연결한 후에 `URI`를 가져오게 됩니다. 해당 `URI`를 이제 `test module`에 입력해주면 사용이 가능해집니다. 

    const module: TestingModule = await Test.createTestingModule({
      imports: [
        MongooseModule.forRoot(MongodbHelper.getUri()),
        MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
      ],
      providers: [AuthService],
    }).compile();
  });

 

연결 해줌과 동시에 기존에 사용하던 `Schema`도 사용해야 하므로 이미 만들어진 스키마도 적용해주면 완료가 된 것입니다. 

 

memory-cache

`Cache`를 적용한다고 하면 자연스럽게 `Redis`를 사용하게 됨으로써 서버에 저장하는 방식보다는 다른 서버에 저장하여 사용하고 있을 것입니다. 그렇기 때문에 기존에 가진 `MongoDB`와 같은 문제가 발생하고 이를 해결하기 위해 다른 방식을 사용을 해야 할 것 입니다.

 

다행스럽게도 `Nest.js`에서는 `In-memory`기능을 기본적으로 제공을 하고 있어 아주 간단하게 사용을 할 수 있습니다. 

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

테스트용 모듈에도 사용하려고 하면 다음과 같습니다. 

  let service: AuthService;
  let userRepository: UserRepository;

  beforeAll(async () => {
    await MongodbHelper.start();

    const module: TestingModule = await Test.createTestingModule({
      imports: [
        CacheModule.register(),
        MongooseModule.forRoot(MongodbHelper.getUri()),
        MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
      ],
      providers: [AuthService],
    }).compile();

    service = module.get<AuthService>(AuthService);
  });

 

이렇게 메모리 디비와 캐시를 적용함으로써 기존에 가지고 있던 문제를 해결하고 처리 할 수 있었습니다. 이러한 방식이 가져오는 다른 이점은 바로 `CI/CD`환경에서의 테스트 코드 검증일 것입니다. 다른 환경에서도 테스트 코드를 작동하였을 때 의존관계를 가지고 있지 않으니 제대로 테스트가 검증이 되고 있다는 것을 인증이 될 수 있다는 것입니다. 

 

 

'Backend > Nest.js' 카테고리의 다른 글

Nest.js Bull  (0) 2023.12.05
Nest.js Integration Test 와 Mocking  (1) 2023.10.10
Nest.js Rate Limiting 설정  (0) 2023.09.30
Nest.js Swagger 보안 설정  (0) 2023.09.30
Nest.js bcrypt (hashing)  (0) 2023.09.27