트렌젝션 적용하는 방법
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: 북마크 [우리집]`으로 테스트 시 정상 작동 여부 확인.
오늘의 문답과 코드가 잘 정리되었기를 바랍니다! 추가 요청이 있으면 언제든 말씀해주세요.
'게임서버-스파르타코딩NodeJs_7기 > CH6 최종 프로젝트' 카테고리의 다른 글
오늘 물은 내용 -Geoadd데이터 저장할 때 (0) | 2025.03.04 |
---|---|
도커 핑퐁 (0) | 2025.03.04 |
GEO 발키 사용 (0) | 2025.03.04 |
기존 좌표 데이터 geo발키로 수정 (0) | 2025.03.03 |
Geo 로 거리 비교 (0) | 2025.03.03 |