주니어 개발자 1호

node-json-db를 TypeORM과 비슷하게 만들어 보기 도전 본문

Server 관련

node-json-db를 TypeORM과 비슷하게 만들어 보기 도전

No_1 2023. 5. 30. 18:56

간단하게 활용할 db가 없나? 라는 과정중에서 node-json-db 라는 라이브러리를 알게 되었습니다.

하지만 그냥 일반 node project와 동일하게 생성자를 통해 관리하기보단, NestJS로 Module를 시켜보고 싶었습니다.

아래 문서를 참고해서 진행할 수 있을 것 같아요.

DynamicModule: https://docs.nestjs.com/fundamentals/dynamic-modules

CustomProviders: https://docs.nestjs.com/fundamentals/custom-providers

( *TypeORM 내부 안보고 도전!! )

//구현 목표
JsonDBModule.forRoot({path:'경로'}),
JsonDBModule.forFeature([Entity,... ])

 

프로젝트 세팅

NestJs에 대한 프로젝트 세팅을 진행하겠습니다.

nest new practice-dynamic-module

node-json-db 설치 및 경로세팅

링크: https://github.com/Belphemur/node-json-db

설치를 진행해줍니다.

npm i node-json-db

경로를 설정해줍니다.

테스트 용도의 목적이니, library pattern을 사용하지 않고 src아래에 위치 시킬려고 합니다.

src/common/json-db.module.ts
src/common/json-db.serivce.ts

그리고 class에 대한 기본 설정을 진행해줍니다.

import {Module} from "@nestjs/common";

@Module({})
export class JsonDbModule {}

--- 
import {Injectable} from "@nestjs/common";

@Injectable()
export class JsonDbService {
    constructor() {}

}

그리고 생성하기 위한 provider가 무엇이 필요한지 파악하기 위해 library를 살펴봅니다.

 

  1. 첫 번째 인수는 데이터베이스 파일명입니다. 확장자를 사용하지 않으면 '.json'으로 가정하여 자동으로 추가합니다.
  2. 두 번째 인수는 푸시할 때마다 DB에 저장하도록 지시하는 데 사용됩니다. 두 번째 인수를 false로 설정하면 save() 메서드를 호출해야 합니다.
  3. 세 번째 인수는 사람이 읽을 수 있는 형식으로 데이터베이스를 저장하도록 JsonDB에 요청하는 데 사용됩니다. (기본값은 false)
  4. 마지막 인수는 구분 기호입니다. 기본값은 슬래시(/)입니다.


라고 합니다.

 

 

forRoot 만들어보기

파악해보니 2, 3, 4에 대해서는 default 값을 사용하고 1은 사용자에게 받아야 합니다. Module을 등록할 때 위치 값을 조정하게 해봅니다.

// JsonDBService 
import { Inject, Injectable } from '@nestjs/common';
import { JsonDB } from 'node-json-db';

@Injectable()
export class JsonDBService {
  private db: JsonDB;
  private fileName: string = '';

  constructor(@Inject('DB_PATH') private dbPath: { path: string }) {
    console.log('dbPath', dbPath);
  }
}

그리고 공급을 받을 수 있는지 app.service.ts에서 확인 해 보기 위해 Module에 장착해줍니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { JsonDBService } from './common/json-DB.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    JsonDBService,
		// 공급!! 
    {
      provide: 'DB_PATH',
      useValue: {
        path: './data',
      },
    },
  ],
})
export class AppModule {}

네, 아래 사진과 같이 정상적으로 공급 받는 것을 확인했습니다.

 

 

그럼 해당 부분에 대한 forRoot에 대한 메소드를 생성하여 공급할 수 있도록 합니다. 그리고 다른 Module에서도 DB_PATH를 공급할 수 있도록 Global Decorator로 처리합니다.

import { DynamicModule, Module } from '@nestjs/common';

@Global()
@Module({})
export class JsonDBModule {
  static forRoot(path: { path: string }): DynamicModule {
    return {
      module: JsonDBModule,
      providers: [
        {
          provide: 'DB_PATH',
          useValue: path,
        },
      ],
      exports: ['DB_PATH'],
    };
  }
}

를 통해 JsonDBModule.forRoot를 통해 path에 대한 inject를 수행했습니다. 이제 AppModule에 장착하여 줍니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { JsonDBModule } from './common/json-DB.module';

@Module({
  imports: [JsonDBModule.forRoot({ path: '/data' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

forFeature 만들어보기

이제 가상의 Entity를 만들고 이것을 사용하기 위해 forFeature를 만들어보겠습니다.

static forFeature(entities: any): DynamicModule {
    const providers = entities.map((value) => ({
      provide: value.name,
      useFactory: (path: { path: string }) => {
        return new JsonDBService<typeof value>(path);
      },
      inject: ['DB_PATH'],
    }));

    return {
      module: JsonDBModule,
      providers,
      exports: entities.map((entity) => entity.name),
    };
  }

공급되는 provide는 아직 타입등이 추상화되지 않았습니다. 일련의 테스트과정 이후 Interface등을 깔끔하게 나누어볼게요.

그리고 테스트를 하기 위해 필요한 UserEntity, BoardEntity등을 만들겠습니다.

src/entities/user.entity.ts
src/entities/board.entity.ts
export class UserEntity {
	id: string
  name: string;
  age: number;
}

export class BoardEntity {
    id: number;
		userId: string;
    title: string;
    description: string;
}

그리고 User에 관한 resource를 생성하여 줍니다.

nest g res user

그리고 entity는 만들었기에 user/entites 에 위치한 폴더는 삭제합니다.

이제 forFeature를 장착해보겠습니다.

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { JsonDBModule } from '../common/json-DB.module';
import { UserEntity } from '../entites/user.entity';
import { BoardEntity } from '../entites/board.entity';

@Module({
  imports: [JsonDBModule.forFeature([UserEntity, BoardEntity])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

이상없이 App이 동작함을 확인합니다.

 

 

이후, UserService에서 필요한 사항을 주입합니다.

@Injectable()
export class UserService {
  constructor(
    @Inject(UserEntity.name)
    private readonly userJsonDBService: JsonDBService<UserEntity>,
  ) {}

  //...
}

그리고 json-db를 만들어야 하기에 JsonDBService에 대해 생성자에서 db를 만드는 것을 수행하겠습니다. any 타입은 추후 추상화를 통해 없앨 예정 입니다. forFeature에서도 entity인자를 넘겨 줍니다.

import { Inject, Injectable } from '@nestjs/common';
import { Config, JsonDB } from 'node-json-db';

@Injectable()
export class JsonDBService<T> {
  private db: JsonDB;
  private fileName: string = '';

  constructor(
    @Inject('DB_PATH') private dbPath: { path: string },
    entity: any,
  ) {
    this.db = new JsonDB(
      new Config(`${dbPath.path}/${entity.name}`, true, true, '/'),
    );
  }
}
static forFeature(entities: any): DynamicModule {
    const providers = entities.map((value) => ({
      provide: value.name,
      useFactory: (path: { path: string }) => {
        return new JsonDBService<typeof value>(path, value);
      },
      inject: ['DB_PATH'],
    }));

    return {
      module: JsonDBModule,
      providers,
      exports: entities.map((entity) => entity.name),
    };
  }

이를 통해 dbPath: data 인 곳에서 파일이 생성 되었습니다.

 

 

Type 추상화로 깔끔하게 하기

이 때 까지 쓰였던 any Type 회수와 forFeature에 대한 제약을 걸어보겠습니다.

아래 파일을 생성 하여 줍니다.

src/common/entity.type.ts

그리고 Entity에 대한 모음 파일을 아래에서 export 해 줍니다.

src/common/config/db.config.ts

//---
import { BoardEntity } from '../../entites/board.entity';
import { UserEntity } from '../../entites/user.entity';

export const DB_MODULES = [BoardEntity, UserEntity];

이제 entity.type.ts에서 추상화를 진행하겠습니다.

import { DB_MODULES } from '../type/entity.type';

export type Entity = typeof DB_MODULES[number];

 

 

Entity에 대한 type을 뽑았습니다.

이를 토대로

JsonDBModule의 forFeature에서 inputArgument Type을 수정해줍니다.

(entities: any) => 
---
(entities: Entity[])

// 그리고 useFactory에서 generic을 활용합니다.
useFactory: (path: { path: string }) => {
    return new JsonDBService<typeof value>(path, value);
},

마지막으로 JsonDBService에서 entity:any를 수정해줍니다.

entity: any, 
--> 
entity: Entity,

테스트

마지막 테스트를 진행해보겠습니다.

UserService에서 User에 대해 저장하고 데이터를 확인하는 것을 진행합니다.

DBSerivce에 메소드에 대한 구현을 합니다.

async getData(key: string): Promise<T> {
    return this.db.getData(key);
  }
  async saveData(key: string, data: T): Promise<void> {
    await this.db.push(`/${key}`, data, true);
    await this.db.save();
  }

UserCreate 기능을 만들어 줍니다.

@Inject(UserEntity.name)
    private readonly userJsonDBService: JsonDBService<UserEntity>,
... 

createUser(){
  const user = new UserEntity();
  user.name = 'test';
  user.age = 10;

  this.userJsonDBService.saveData('user[]/', user);
}

테스트를 진행하여, 결과를 봅니다.

{
    "user": [
        {
            "name": "test",
            "age": 10
        },
        {
            "name": "test",
            "age": 10
        }
    ]
}

잘 들어가는 것을 확인하였습니다.

출력도 확인을 해보겠습니다.

this.userJsonDBService.getData('user[]/').then((data) => {
      console.log(data);
    });

로 요청하여 아래 값을 받았습니다.

{ user: [ { name: 'test', age: 10 }, { name: 'test', age: 10 } ] }

각 필요한 부분에 interface를 만들어 추상화를 통하여 고도화를 진행할 수 있을 듯 합니다.