본문 바로가기
개발 일지/카카오VX

전 서비스 TypeOrm 버전 업

by StelthPark 2024. 8. 16.

작업 배경

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 처리 서비스의 재사용성을 높일 수 있게 하였다. 이전에 개발되어있던 트랜잭션 서포트를 다시 한번 파헤쳐보면서 트랜잭션 관리에 대해 더 이해할 수 있는 유익한 시간이였다.

댓글