본문 바로가기
게임서버-스파르타코딩NodeJs_7기/CH6 최종 프로젝트

트렌젝션 적용하는 방법

by GREEN나무 2025. 3. 5.
728x90

트렌젝션 적용하는 방법

 

 

1. 트랜잭션 적용 요청

- 질문: `AchievementPService.post` 메서드에 트랜잭션을 적용해달라는 요청.

- 답변: TypeORM`QueryRunner`를 사용해 트랜잭션을 구현. `startTransaction`, `commitTransaction`, `rollbackTransaction`을 추가하고, 모든 DB 작업을 트랜잭션 내에서 처리하도록 수정.

- 추가 조언: `AchievementPRepository``getQueryRunner()` 메서드가 필요하며, 이를 위해 `DataSource` 주입이 필요함을 언급.

 

2. `getQueryRunner()` 메서드 추가

- 질문: `AchievementPRepository``getQueryRunner()` 메서드를 추가해달라는 요청.

- 답변: `DataSource`를 주입받아 `getQueryRunner()`를 구현. `DataSource`를 모듈에서 제공하기 위해 `AchievementPModule`에 커스텀 프로바이더를 추가하는 방법을 설명.

- 코드: `dataSource.createQueryRunner()`를 반환하도록 구현.

 

3. `getCustomRepository` 관련 오류

- 질문: `'queryRunner.manager.getCustomRepository'`가 사용되지 않는다는 TypeScript 경고 발생.

- 답변: `getCustomRepository`TypeORM 0.2.x의 레거시 방식으로, 0.3.x에서는 `Repository.extend`나 기본 Repository 사용을 권장. `AchievementPRepository` 메서드에 `queryRunner`를 옵셔널 파라미터로 추가해 트랜잭션 내에서 동작하도록 수정.

- 해결: `queryRunner.manager.getRepository()`를 사용해 기본 Repository를 호출하도록 변경.

 

4. `DataSource` 이름 오류

- 질문: `AchievementPModule`에서 `'DataSource' 이름을 찾을 수 없습니다` 오류 발생.

- 답변: `DataSource``typeorm`에서 임포트하지 않아 발생한 문제. `import { DataSource } from 'typeorm'`을 추가해 해결.

- 추가 설명: `provide: 'DATA_SOURCE'`의 역할과 `useFactory`, `inject`의 동작 원리를 상세히 설명.

 

5. `CustomRepositoryNotFoundError` 발생

- 질문: 실행 중 `CustomRepositoryNotFoundError` 발생. `@EntityRepository`가 필요하다는 메시지.

- 답변: 실행 중인 코드가 최신 수정사항을 반영하지 않았거나, TypeORM 버전이 0.2.x일 가능성. 최신 코드로 동기화하고, `getCustomRepository` 호출을 제거한 상태를 확인하라고 제안. TypeORM 버전 확인 및 프로젝트 재빌드 권장.

- 해결: `AchievementPService``AchievementPRepository`를 최신 버전으로 동기화.

 

---

 

최종 정리된 코드

 

`AchievementPService`

트랜잭션을 적용하고, `getCustomRepository`를 제거한 최신 버전:

```typescript

import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';

import { AchievementP } from './entities/achievement-p.entity';

import { AchievementPRepository } from './achievement-p.repository';

import { ValkeyService } from '../valkey/valkey.service';

import { QueryRunner } from 'typeorm';

 

@Injectable()

export class AchievementPService {

constructor(

private readonly repository: AchievementPRepository,

private readonly valkeyService: ValkeyService,

) {}

 

async post(user_id: number, subId: number): Promise<AchievementP> {

const queryRunner = this.repository.getQueryRunner();

await queryRunner.connect();

await queryRunner.startTransaction();

 

try {

if (!subId) {

throw new BadRequestException('subAchievementId 값이 없거나 형식이 맞지 않습니다');

}

 

const isSubId = await this.repository.findSub(subId, queryRunner);

if (!isSubId) {

throw new NotFoundException('해당 서브업적이 존재하지 않습니다.');

}

 

const alreadyP = await this.repository.findPByUserNSub(user_id, subId, queryRunner);

if (alreadyP) {

throw new BadRequestException('이미 달성한 서브업적 입니다.');

}

 

const dataP = {

user_id,

sub_achievement_id: subId,

achievement_id: isSubId?.achievement_id ?? null,

complete: true,

};

const createP = await this.repository.createP(dataP, queryRunner);

const save = await this.repository.save(createP, queryRunner);

if (!save) {

throw new BadRequestException('P저장 실패했습니다.');

}

 

const subAchievements = (await this.repository.subAllByA(isSubId.achievement_id, queryRunner)).map(

(sub) => sub.id,

);

if (!subAchievements || subAchievements.length < 1) {

throw new BadRequestException('서브목록 조회 실패했습니다.');

}

 

const achievedP = (await this.repository.pAllByA(isSubId.achievement_id, queryRunner)).map(

(sub) => sub.sub_achievement_id,

);

if (!achievedP || achievedP.length < 1) {

throw new BadRequestException('P목록 조회 실패했습니다.');

}

 

if (subAchievements.length === achievedP.length) {

const isMatching = subAchievements.every((id) => achievedP.includes(id));

if (isMatching) {

await this.repository.createC(

{ user_id, achievement_id: isSubId.achievement_id },

queryRunner,

);

 

const reward = await this.repository.reward(isSubId.achievement_id, queryRunner);

const gem = Number(reward.reward.gem);

if (!gem) {

throw new BadRequestException('gem 값이 없거나 형식이 맞지 않습니다');

}

await this.repository.gem(user_id, gem, queryRunner);

 

const dia = Number(reward.reward.dia);

if (!dia) {

throw new BadRequestException('dia 값이 없거나 형식이 맞지 않습니다');

}

await this.repository.dia(user_id, dia, queryRunner);

 

console.log(`핑크다이아 ${dia}개와 핑크젬 ${gem}개가 지급되었습니다.`);

}

}

 

await queryRunner.commitTransaction();

return save;

} catch (error) {

await queryRunner.rollbackTransaction();

throw error;

} finally {

await queryRunner.release();

}

}

}

```

 

`AchievementPRepository`

`queryRunner`를 지원하며, 최적화된 버전:

```typescript

import { Injectable } from '@nestjs/common';

import { Repository, DataSource, QueryRunner } from 'typeorm';

import { InjectRepository } from '@nestjs/typeorm';

import { AchievementP } from './entities/achievement-p.entity';

import { SubAchievement } from '../sub-achievement/entities/sub-achievement.entity';

import { AchievementC } from '../achievement-c/entities/achievement-c.entity';

import { User } from '../user/entities/user.entity';

import { Achievement } from '../achievement/entities/achievement.entity';

import { RewardAchievementC } from './dto/reword-achievement-p.dto';

 

@Injectable()

export class AchievementPRepository {

constructor(

@InjectRepository(Achievement)

private readonly entityA: Repository<Achievement>,

@InjectRepository(AchievementP)

private readonly entityP: Repository<AchievementP>,

@InjectRepository(AchievementC)

private readonly entityC: Repository<AchievementC>,

@InjectRepository(SubAchievement)

private readonly entityS: Repository<SubAchievement>,

@InjectRepository(User)

private readonly entityU: Repository<User>,

@Inject('DATA_SOURCE')

private readonly dataSource: DataSource,

) {}

 

getQueryRunner(): QueryRunner {

return this.dataSource.createQueryRunner();

}

 

async findSub(id: number, queryRunner?: QueryRunner): Promise<SubAchievement | null> {

const repo = queryRunner ? queryRunner.manager.getRepository(SubAchievement) : this.entityS;

return await repo.findOne({ where: { id } });

}

 

async findPByUserNSub(user_id: number, idS: number, queryRunner?: QueryRunner): Promise<AchievementP | null> {

const repo = queryRunner ? queryRunner.manager.getRepository(AchievementP) : this.entityP;

return await repo.findOne({ where: { user_id, sub_achievement_id: idS } });

}

 

async createP(data: Partial<AchievementP>, queryRunner?: QueryRunner): Promise<AchievementP> {

const repo = queryRunner ? queryRunner.manager.getRepository(AchievementP) : this.entityP;

return repo.create(data);

}

 

async save(data: AchievementP, queryRunner?: QueryRunner): Promise<AchievementP> {

const repo = queryRunner ? queryRunner.manager.getRepository(AchievementP) : this.entityP;

return await repo.save(data);

}

 

async subAllByA(achievement_id: number, queryRunner?: QueryRunner): Promise<SubAchievement[]> {

const repo = queryRunner ? queryRunner.manager.getRepository(SubAchievement) : this.entityS;

return await repo.find({ where: { achievement_id } });

}

 

async pAllByA(achievement_id: number, queryRunner?: QueryRunner): Promise<AchievementP[]> {

const repo = queryRunner ? queryRunner.manager.getRepository(AchievementP) : this.entityP;

return await repo.find({ where: { achievement_id } });

}

 

async createC(data: Partial<AchievementC>, queryRunner?: QueryRunner): Promise<AchievementC> {

const repo = queryRunner ? queryRunner.manager.getRepository(AchievementC) : this.entityC;

const achievementC = repo.create(data);

return await repo.save(achievementC);

}

 

async reward(achievementId: number, queryRunner?: QueryRunner): Promise<RewardAchievementC> {

const repo = queryRunner ? queryRunner.manager.getRepository(Achievement) : this.entityA;

const achievement = await repo.findOne({ where: { id: achievementId }, select: ['reward'] });

if (achievement?.reward) {

const rewardData = typeof achievement.reward === 'string' ? JSON.parse(achievement.reward) : achievement.reward;

return { reward: rewardData };

}

return { reward: { gem: 0, dia: 0 } };

}

 

async gem(user_id: number, gem: number, queryRunner?: QueryRunner) {

const repo = queryRunner ? queryRunner.manager.getRepository(User) : this.entityU;

return await repo

.createQueryBuilder()

.update()

.set({ pink_gem: () => 'pink_gem + :gem' })

.where('id = :user_id', { user_id })

.setParameter('gem', gem)

.execute();

}

 

async dia(user_id: number, dia: number, queryRunner?: QueryRunner) {

const repo = queryRunner ? queryRunner.manager.getRepository(User) : this.entityU;

return await repo

.createQueryBuilder()

.update()

.set({ pink_dia: () => 'pink_dia + :dia' })

.where('id = :user_id', { user_id })

.setParameter('dia', dia)

.execute();

}

}

```

 

`AchievementPModule`

`DataSource`를 제공하는 최종 모듈:

```typescript

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

import { TypeOrmModule } from '@nestjs/typeorm';

import { AchievementPService } from './achievement-p.service';

import { AchievementPController } from './achievement-p.controller';

import { AchievementPRepository } from './achievement-p.repository';

import { AchievementP } from './entities/achievement-p.entity';

import { User } from '../user/entities/user.entity';

import { SubAchievement } from '../sub-achievement/entities/sub-achievement.entity';

import { AchievementC } from '../achievement-c/entities/achievement-c.entity';

import { Achievement } from '../achievement/entities/achievement.entity';

import { ValkeyModule } from 'src/valkey/valkey.module';

import { DataSource } from 'typeorm';

 

@Module({

imports: [

TypeOrmModule.forFeature([AchievementP, User, SubAchievement, AchievementC, Achievement]),

ValkeyModule,

],

controllers: [AchievementPController],

providers: [

AchievementPService,

AchievementPRepository,

{

provide: 'DATA_SOURCE',

useFactory: (dataSource: DataSource) => dataSource,

inject: [DataSource],

},

],

exports: [AchievementPService, AchievementPRepository],

})

export class AchievementPModule {}

```

 

---

 

정리된 코드의 특징

1. 트랜잭션: 모든 DB 작업이 트랜잭션 내에서 실행되며, 오류 시 롤백 처리.

2. 최신 TypeORM: `getCustomRepository` 대신 기본 Repository를 사용하며, TypeORM 0.3.x와 호환.

3. 의존성 주입: `DataSource`를 모듈에서 제공하여 `QueryRunner` 생성 가능.

4. 에러 핸들링: 필요한 검증과 예외 처리가 포함됨.

5. 가독성: 변수명과 로그 메시지를 간결하게 정리.

 

---

 

최종 확인사항

- TypeORM 버전: `package.json`에서 `"typeorm": "^0.3.x"`인지 확인.

- 프로젝트 빌드: `npm run build`로 최신 코드를 반영.

- 테스트: `user_id: 6`, `subId: 북마크 [우리집]`으로 테스트 시 정상 작동 여부 확인.

 

오늘의 문답과 코드가 잘 정리되었기를 바랍니다! 추가 요청이 있으면 언제든 말씀해주세요.