작업 배경
API 서비스마다 어떤 서비스는 TypeOrm 버전이 0.2.x대를 사용중이였고 어떤 서비스는 0.3.x 대를 사용중이였다. 이를 같은 0.3.x버전대로 맞추는 작업을 진행하였다.
TypeOrm 0.3 주요 변경 사항
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에 연결할 수 있게 됩니다.
0.2.x에선 @EntityRepository 를 이용하여 간단히 사용하던 방식이 @Injectable 를 이용하여 DataSource를 통하여 Repository를 생성하도록 변경되었습니다.
where 키를 사용해주지 않았을 경우 |
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로 버전업하기
- 주요사항
- 0.3.x에서는 @EntityRepository가 사라지고 이로 인해 커스텀 레포지토리 패턴을 사용할 수 없게 되었고 이에 따라 커스텀 레포지토리 패턴를 비슷하게 사용하는 방법으로 수정하였습니다.
- Connection 옵션이 사라지고 dataSource 나옴에 따라 비즈니스 서비스 내에서 Connection 옵션을 사용하여 직접 db에 접근하는 방식에서 레포지토리를 생성하여 db접근을 해당 레포지토리에서만 하도록 수정하였습니다.
- 새로 탄생한 dataSource를 의존성 주입 받아 트랜잭션을 핸들링하는 클래스를 사용하였습니다.
- 기존의 코드 구조를 최대한 바꾸지 않고 typeorm과 nestjs/typeorm 버전을 업을 하였습니다.
- 코드 수정
- @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 처리 서비스의 재사용성을 높일 수 있게 하였다. 이전에 개발되어있던 트랜잭션 서포트를 다시 한번 파헤쳐보면서 트랜잭션 관리에 대해 더 이해할 수 있는 유익한 시간이였다.
'개발 일지 > 카카오VX' 카테고리의 다른 글
서비스 중심 kafka와 redis pub/sub (0) | 2024.08.26 |
---|---|
온체인마켓 LOG 적재 Apm+Kibana (0) | 2024.08.16 |
실시간 유저 재화를 위한 kafka 도입 (0) | 2024.08.16 |
젠킨스로 CI/CD 자동화 구성 (0) | 2024.08.16 |
댓글