본문 바로가기

[Project] 프로젝트 삽질기8 (feat ormConfig)

어가며

사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 먼저 NestJS와 TypeORM을 활용해서 개발을 하고 있는데, TypeORM을 설정할 때 활용하는 ormConfig에서 autoLoadEntities가 어떤  역할을 하는지 궁금했습니다. 이 글은 TypeORM이 엔티티와 연동되는 과정(nest.js) 글을 참고했습니다. 

 

 

 

 

 


 

 

 

 

 

 

 

 

TypeORM 설정

TypeORM을 활용하기 위해서는 ormconfig를 설정해야 합니다. ormconfig에는 다양한 옵션이 있습니다. 예를 들면 다음과 같은 옵션이 존재합니다.

 

export declare type TypeOrmModuleOptions = {
    retryAttempts?: number;
    retryDelay?: number;
    toRetry?: (err: any) => boolean;
    autoLoadEntities?: boolean;
    keepConnectionAlive?: boolean;
    verboseRetryLog?: boolean;
} & Partial<ConnectionOptions>;

 

  • retryAttempts: 연결 시 재시도 회수. 기본값은 10입니다.
  • retryDelay: 재시도 간의 지연 시간. 단위는 ms이고 기본값은 3000입니다.
  • toRetry: 에러가 났을 때 연결을 시도할지 판단하는 함수. 콜백으로 받은 인자 err 를 이용하여 연결여부를 판단하는 함수를 구현하면 됩니다.
  • autoLoadEntities: 엔티티를 자동 로드할 지 여부.
  • keepConnectionAlive: 애플리케이션 종료 후 연결을 유지할 지 여부.
  • verboseRetryLog: 연결을 재시도할 때 verbose 에러메시지를 보여줄 지 여부. 로깅에서 verbose 메시지는 상세 메시지를 의미합니다.

 

위의 내용 중, autoLoadEntities의 내용이 궁금했습니다. 엔티티를 자동 로드할 지의 여부를 판단한다는데, 여기서 엔티티의 자동 로드란 무엇일까 궁금했습니다. 

 

 

 

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

 

 

위 코드에서 forRoot에 들어가는 객체는 ormconfig.json에 들어가는 값과 같습니다. 보통 프로젝트 내에서 사용하는 모든 entitiy를 참조하기 위해 사용하는 관례 코드는 다음가 같습니다.

 

 

entities: './dist/**/*.entity.js'

 

보통 NestJS 프로젝트를 타입스크립트로 작업하면, 개발환경에서 watch 모드로 실시간 빌드를 해주기 때문에 dist 폴더에 트랜스파일링된 js 파일들이 만들어집니다. 여기에서 entity 파일들을 스캔하여 엔티티들을 실제 DB와 연결해줍니다. 하지만 NestJS를 모노레포 방식으로 변경하여 작업하면, 웹팩 빌드로 변경되기 때문에 dist에 파일이 하나로 뭉쳐져서 만들어집니다. 기존의 방식으로 파일 경로로 entity들을 스캔할 수 없게 됩니다.

 

따라서 Nest 팀은 이를 인지하고 @nestjs/typeorm 패키지에 autoLoadEntities 속성을 만들어 제공해주고 있습니다. 이를 이용하면 더 쉽게 엔티티를 연결할 수 있습니다. 그럼 대체 autoLoadEntities 속성은 어떻게 entitiy 들을 스캔하는 것일까요? 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

코드베이스로 엔티티 참조

먼저 @nestjs/typeorm 소스를 보면 autoLoadEntities가 어떻게 구현되어 있는지 알 수 있습니다.

 

 

private static async createConnectionFactory(
  options: TypeOrmModuleOptions,
  connectionFactory?: TypeOrmConnectionFactory,
): Promise<Connection> {
  const connectionToken = getConnectionName(options as ConnectionOptions);
  ...
      if (!options.autoLoadEntities) {
        return createTypeormConnection(options as ConnectionOptions);
      }
      // autoLoadEntities 옵션이 있을 경우 아래 코드를 수행
      let entities = options.entities;
      // entities 속성이 존재한다면 뒤에 붙여준다.
      if (entities) {
        // EntitiesMetadataStorage 라는 함수를 통해 엔티티 리스트를 가져와 추가해준다.
        entities = entities.concat(
          EntitiesMetadataStorage.getEntitiesByConnection(connectionToken),
        );
      } else {
        entities =
          EntitiesMetadataStorage.getEntitiesByConnection(connectionToken);
      }
      return createTypeormConnection({
        ...options,
        entities,
      } as ConnectionOptions);
   ...

 

 

 

위 코드를 보면 autoLoadEntities가 참일 시 EntitiesMetadataStorage.getEntitiesByConnection(connectionToken)를 호출하여 엔티티 리스트를 가져와 합쳐주는 것을 볼 수 있습니다. 함수명으로 짐작건대, EntitiesMetadataStorage라는 글로벌 저장소에서 엔티티를 가져올 것입니다. 위에서 사용된 EntitiesMetadataStorage 클래스도 @nestjs/typeorm에 구현되어 있습니다.

 

 

 

import { Connection, ConnectionOptions } from 'typeorm'
import { EntityClassOrSchema } from './interfaces/entity-class-or-schema.type'

type ConnectionToken = Connection | ConnectionOptions | string

export class EntitiesMetadataStorage {
  private static readonly storage = new Map<string, EntityClassOrSchema[]>()

  static addEntitiesByConnection(
    connection: ConnectionToken,
    entities: EntityClassOrSchema[]
  ): void {
    const connectionToken = typeof connection === 'string' ? connection : connection.name
    if (!connectionToken) {
      return
    }

    let collection = this.storage.get(connectionToken)
    if (!collection) {
      collection = []
      this.storage.set(connectionToken, collection)
    }
    entities.forEach((entity) => {
      if (collection!.includes(entity)) {
        return
      }
      collection!.push(entity)
    })
  }

  static getEntitiesByConnection(connection: ConnectionToken): EntityClassOrSchema[] {
    const connectionToken = typeof connection === 'string' ? connection : connection.name

    if (!connectionToken) {
      return []
    }
    return this.storage.get(connectionToken) || []
  }
}

 

 

EntitiesMetadataStorage 클래스는 정적 함수로만 이루어져있으며, 다음 두 함수가 있습니다.

 

  • addEntitiesByConnection: connectionToken 키 값에 Entity 리스트를 추가합니다.
  • getEntitiesByConnection: connectionToken 키 값 안에 저장된 모든 Entity 리스트를 가져옵니다.

 

즉, 미리 addEntitiesByConnection를 통해 엔티티를 추가하고, getEntitiesByConnection로 가져와 엔티티들을 실제 DB와 연동합니다. 그럼 마지막으로 어디서 addEntitiesByConnection 함수를 호출하는지 살펴보겠습니다.

 

 

@Module({})
export class TypeOrmModule {
  ...
  static forFeature(
    entities: EntityClassOrSchema[] = [],
    connection: Connection | ConnectionOptions | string = DEFAULT_CONNECTION_NAME
  ): DynamicModule {
    const providers = createTypeOrmProviders(entities, connection)
    const customRepositoryEntities = getCustomRepositoryEntity(entities)
    // 함수 호출
    EntitiesMetadataStorage.addEntitiesByConnection(connection, [
      ...entities,
      ...customRepositoryEntities,
    ])
    return {
      module: TypeOrmModule,
      providers: providers,
      exports: providers,
    }
  }
  ...
}

 

TypeOrmModule.forFeature 함수를 통해 엔티티를 넣어줍니다. 우리는 보통 외부 API를 노출하기 위해 여러 커스텀 모듈을 만들고, 그 곳에 TypeOrmModule.forFeature 함수를 통해 엔티티나, 커스텀 레포지토리를 연동합니다. 그 내부 구현에서 EntitiesMetadataStorage에 엔티티를 넣어주는 것이었습니다. 그럼 우리가 작성한 forFeature 함수가 모두 실행되고 마지막으로 AppModule TypeOrmModule.forRoot가 실행되야 겠죠? 이는 어떻게 확인할 수 있을까요? 코드를 분석하며 찾아볼 수도 있겠지만, 더 쉬운 방법이 있습니다. 바로 dev 환경에서 console.log 코드에 넣어봐서 실행하는 것입니다.

 

먼저 app.module.ts 에 다음 코드를 삽입합니다.

 

 

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      useFactory: async () => {
        console.log(`forRoot 실행!`)
        return Object.assign(config, {
          autoLoadEntities: true,
        })
      },
    }),
  ]
})

 

그리고 node_modules 폴더 내부의 @nestjs/typeorm에 들어가 forFeature 구현부에 다음 소스를 추가합니다.

 

 

...
static forFeature(entities = [], connection = typeorm_constants_1.DEFAULT_CONNECTION_NAME) {
  const providers = typeorm_providers_1.createTypeOrmProviders(entities, connection);
  const customRepositoryEntities = get_custom_repository_entity_1.getCustomRepositoryEntity(entities);
  console.log(entities, customRepositoryEntities)
  entities_metadata_storage_1.EntitiesMetadataStorage.addEntitiesByConnection(connection, [
      ...entities,
      ...customRepositoryEntities,
  ]);
  return {
      module: TypeOrmModule_1,
      providers: providers,
      exports: providers,
  };
}
...

 

 

대충 엔티티가 먼저 잘 나오는군요. 위와 같이 forRoot 전에 모든 엔티티가 잘 불러와진다는 것을 확인할 수 있습니다. 이렇게 @nestjs/typeorm에서 autoLoadEntities 값을 참으로 주면 엔티티 로드 프로세스는 다음과 같습니다.

 

  1. forFeature를 통해 EntitiesMetadataStorage 글로벌 스토어에 엔티티를 추가합니다.
  2. 모든 forFeature가 불린 후, forRoot 함수가 실행되어 EntitiesMetadataStorage에 저장된 엔티티 목록을 가져옵니다.
  3. 해당 엔티티 리스트를 typeorm 설정에 넣어 실제 DB와 연결합니다.

 

 

 

 

 


 

 

 

 

 

마치며

앞으로도 팀의 발전을 돕는 개발자가 되기 위해 노력하려 합니다. 팀에 필요한 부분이 무엇일지 고민하면서, 팀에 도움이 된다면, 열심히 공부해서 실무에 적용할 수 있는 개발자가 되기 위해 노력하고 싶습니다. 팀의 성장에 기여할 수 있는 개발자가 되겠습니다. 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

참고 및 출처

 

TypeORM이 엔티티와 연동되는 과정 (nest.js)

본 글은 Nest Docs Database를 토대로 공부하여 작성한 글입니다.nest.js는 SQL, no-SQL 데이터베이스 유명 라이브러리와 호환 가능합니다.주로 많은 사람들이 TypeORM, Sequelize를 연동하여 사용하고 있습니

velog.io