개발 일지/카카오VX

전 서비스 TypeOrm 버전 업

StelthPark 2024. 8. 16. 23:14

작업 배경

API 서비스마다 어떤 서비스는 TypeOrm 버전이 0.2.x대를 사용중이였고 어떤 서비스는 0.3.x 대를 사용중이였다. 이를 같은 0.3.x버전대로 맞추는 작업을 진행하였다.

 

TypeOrm 0.3 주요 변경 사항

사라져 버린 Connection
export const dataSource = new DataSource({ type: "mysql",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "test", })
export const dataSource1 = new DataSource({ /*...*/ })
export const dataSource2 = new DataSource({ /*...*/ })
export const dataSource3 = new DataSource({ /*...*/ })


// load entities, establish db connection, sync schema, etc.
await dataSource.connect()

// destroy dataSource
await dataSource.destroy()

 

dataSource의 등장으로 connection 옵션이 사라지고 DataSource로 대체되었습니다. DataSource를 여러개 연결하면 여러개의 DB에 연결할 수 있게 됩니다.

 

 

사라져버린 EntityRepository

0.2.x에선 @EntityRepository 를 이용하여 간단히 사용하던 방식이 @Injectable 를 이용하여 DataSource를 통하여 Repository를 생성하도록 변경되었습니다.

 


where 키를 사용해주지 않았을 경우

where 키를 사용

 
findOne 등 파라미터 내부에 조건절을 사용 시 where 키를 사용해주어야 합니다.

 

Find - select 절 사용의 변경으로 기존 string array로 된 select 절이 Entity의 property를 지정할수 있게 되었습니다. 그리고 관계된 테이블의 컬럼도 지정할 수 있습니다.

  • true로 찍은 값만 select 되며 false는 의미없다..

 

Where - condtion 절의 Nested 지원으로 기존 지원되지 않던 관계시 nested한 조건을 지정할 수 있습니다.

  • nested한 조건을 걸게 되면 join이 여러개가 되면서 메모리를 많이 잡아먹게 되어 nested한 조건을 사용하지 않겠다는 옵션을 걸고 쓰면 된다. 해당 조건은 join 또는 query 값 2개만 허용한다.
// UserRepository.ts (0.2.x)

userRepository.find({
  select: ["id", "firstName", "lastName"]
})

// UserRepository.ts (0.3.x)

userRepository.find({
    select: {
        id: true,
        firstName: true,
        lastName: true,
        photo: {
            id: true,
            filename: true,
            album: {
                id: true,
                name: true,
            }
        }
    }
    relationLoadStrategy: 'query', //nested한 조건을 사용하지 않겠음!
})
// UserRepository.ts (0.3.x)

userRepository.find({
    where: {
        photos: {
            album: {
                name: "profile"
            }
        }
    }
})

0.3 버전에서 중첩된 관계까지 조건절을 걸 수 있다.


채팅서버 TypeOrm 버전 0.3.X로 버전업하기

  1. 주요사항
    • 0.3.x에서는 @EntityRepository가 사라지고 이로 인해 커스텀 레포지토리 패턴을 사용할 수 없게 되었고 이에 따라 커스텀 레포지토리 패턴를 비슷하게 사용하는 방법으로 수정하였습니다.
    • Connection 옵션이 사라지고 dataSource 나옴에 따라 비즈니스 서비스 내에서 Connection 옵션을 사용하여 직접 db에 접근하는 방식에서 레포지토리를 생성하여 db접근을 해당 레포지토리에서만 하도록 수정하였습니다.
    • 새로 탄생한 dataSource를 의존성 주입 받아 트랜잭션을 핸들링하는 클래스를 사용하였습니다.
    • 기존의 코드 구조를 최대한 바꾸지 않고 typeorm과 nestjs/typeorm 버전을 업을 하였습니다.
  2. 코드 수정
    • @EntityRepository의 역할을 하게 해줄 데코레이터를 만들어준다.
    • 여기서 메타데이터를 사용하는 이유는 Nest에서 DI는 런타임에서 제어하므로 런타임전에 메타데이터 세팅을 해놓고 런타임에 만들어둔 다이나믹 모듈 방식으로 DI를 해줄것이기 때문입니다.
import { SetMetadata } from '@nestjs/common';

export const TYPEORM_CUSTOM_REPOSITORY = 'TYPEORM_CUSTOM_REPOSITORY';

/**
 * @brief EntityRepository을 커스텀한 데코레이터
 * @description 기존 TypeORM 3.0 이하 버전에서 사용하던 EntityRepository을 데코레이터가 없어지면서 커스텀한 데코레이터
 */

export function CustomEntityRepository(entity: Function): ClassDecorator {
  // SetMetadata는 key: value형태이고 TYPEORM_CUSTOM_REPOSITORY가 key가 되고 엔티티가 vaule가 된다.
  return SetMetadata(TYPEORM_CUSTOM_REPOSITORY, entity);
}

 

1. 이제 다이나믹 모듈을 만들어줍니다.

import { DynamicModule, Provider } from '@nestjs/common';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DataSource, DataSourceOptions } from 'typeorm';
import { TYPEORM_CUSTOM_REPOSITORY } from './custom-typeorm.decorator';

/**
 * @brief 커스텀 TypeORM 모듈
 * @description
 */
export class CustomTypeOrmModule {
  public static forCustomRepository<T extends new (...args: any[]) => any>( // 매개변수로 생성자를 포함하는 객체가 필요함
    repositories: T[], // 레포지토리들을 배열형태로 받게된다.
    dataSource?: DataSource | DataSourceOptions | string,
  ): DynamicModule {
    const providers: Provider[] = [];

    //받은 레포지토리들을 for문 돌며
    for (const repository of repositories) {
      // 이전에 데코레이터로 metadata로 set했던걸 get해온다.
      const entity = Reflect.getMetadata(TYPEORM_CUSTOM_REPOSITORY, repository);
      if (!entity) {
        continue;
      }
      // 프로바이더 공급자를 채우게 되며
      providers.push({
        inject: [getDataSourceToken(dataSource)], // DB 연결을 얻음
        provide: repository, // 공급자를 넣으며 레포지토리를 제공
        useFactory: (dataSource: DataSource): typeof repository => {
          // entity를 넣어 레포지토리를 가져온다.
          const baseRepository = dataSource.getRepository<any>(entity); //Reflect.getMetadata에 의해 entity type이 any로 된다.

          return new repository(baseRepository.target, baseRepository.manager, baseRepository.queryRunner);
        },
      });
    }

    return { exports: providers, module: CustomTypeOrmModule, providers };
  }
}

2. 기존 서비스 내에서 직접 엔티티를 연결하던 방식에서 DB연결과 쿼리만 수행하는 레포지토리를 따로만들어 서비스에 인젝트합니다

// 만들어둔 커스텀 엔티티 레포지토리를 사용하여 별개 레포지토리 생성
import { CustomEntityRepository } from 'src/common/custom-typeorm/custom-typeorm.decorator';
import { Repository } from 'typeorm';
import { GAME } from '../entity';

@CustomEntityRepository(GAME)
export class GameRepository extends Repository<GAME> {
  /**
   * @brief 게임 조회
   * @description
   */
  async getOne(gameCode: number): Promise<GAME> {
    return await this.findOne({ where: { GAME_CODE: gameCode } });
  }
}
// chat 서버 내 eventgate.service
import { GameRepository } from 'src/database/repository/game.repository';
export class EventGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
  @WebSocketServer()
  constructor(
    private readonly gameRepository: GameRepository, //만든 레포지토리 인젝트
  ) {
   }
  async test () {
  await.this.gameRepository.getOne(80016347)
    }
  }
  • 기존 로직과 비교해보자면, DB 접근 관련 로직은 레포지토리에서 처리하도록 하였습니다.
  • 기존 0.2.x 버전 방식
// event.gateway.ts
 const dbGAME = await this._gameRepo.findOne({
      select: ['SEQ_NO', ...],
      where: { GAME_CODE },
    });
  • 신규 0.3.x 버전 방식
// event.gateway.ts
 const dbGAME = await this.gameRepository.getOne(GAME_CODE);
// game.repository.ts 
  import { CustomEntityRepository } from 'src/common/custom-typeorm/custom-typeorm.decorator';
  import { Repository } from 'typeorm';
  import { GAME } from '../entity';
  
  @CustomEntityRepository(GAME)
  export class GameRepository extends Repository<GAME> {
    async getOne(gameCode: number): Promise<GAME> {
      return await this.findOne({
        select: ['SEQ_NO', 'startDate', ...],
        where: { GAME_CODE: gameCode },
      });
    }
  }

 

3. 기존 모듈에서 Typeorm.forFeature를 사용하던 것을 아까 만든 CustomTypeOrmModule 사용하여 레포지토리를 달아줍니다.

// 이벤트 게이트 모듈

@Module({
  imports: [
    ExternalsModule,
    CustomTypeOrmModule.forCustomRepository([
      UserRepository,
      ...
    ]),
  ],
  providers: [TransactionSupport],
  exports: [ExternalsModule],
})
export class EventModule {}

4. Connection 옵션이 사라짐에 따라 비즈니스 서비스 내에서 db접근,수정 등을 했었지만 dataSource를 사용하여 트랜잭션을 핸들링 하는 클래스를 생성하였습니다.

  • 기존 코드에서 dbSuccess 플래그 변수를 사용하여 트랜잭션 성공 실패에 따라 값을 바꾸어 SocketException 에러를 띄우는 코드를 추가하였습니다.

 

@Injectable()
export class TransactionSupport {
  constructor(private readonly dataSource: DataSource) {}
  async transact<R = unknown>(func: AsyncFunctionType, errorCallback: ErrorCallbackFunctionType = defaultHandleError): Promise<R> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    //트랜잭션 시작
    await queryRunner.startTransaction();
    try {
      const res = (await func(queryRunner)) as R;
      await queryRunner.commitTransaction();
      return res;
    } catch (e) {
      //에러시 롤백
      await queryRunner.rollbackTransaction();
      errorCallback(e);
      throw new SocketException('BadRequest', ErrDesc.BAD_REQUEST__WRITE_DB_OPERATION);
    } finally {
      await queryRunner.release();
    }
  }
}

5. 이벤트 게이트 내에서 TransactionSupport 클래스 사용

    await this.transaction.transact(async (queryRunner: QueryRunner) => {
      // 아이템 삭제
      await queryRunner.manager.save(USER_ITEM, dbITEM);
      }
    )

 


REVIEW

실제로 TypeOrm 0.3.x버전에서 많은 변화가 있었다. 버전업을 해보면서 connection 방식이나 커스텀 데코레이터를 만드는 방법에 대해 더 잘 알게 되었고 DB에 연결되는 구성이나 구조도 더 잘 알게 되었다. DB에 관련된 로직 처리는 별개 repository 파일을 만들어 내부에서만 처리하고 비즈니스 로직에서는 DB처리 로직을 직접적으로 사용하지 않게하면서 DB 처리 서비스의 재사용성을 높일 수 있게 하였다. 이전에 개발되어있던 트랜잭션 서포트를 다시 한번 파헤쳐보면서 트랜잭션 관리에 대해 더 이해할 수 있는 유익한 시간이였다.