이번에는 데이터 일관성을 위한 트랜잭션 관리로 리팩토링 해보려고 한다
// 명예의 전당 매서드 일주일마다 업데이트
@Cron(CronExpression.EVERY_WEEK)
async updateHallOfFame() {
const lastWeekStart = new Date();
lastWeekStart.setDate(lastWeekStart.getDate() - 7 - lastWeekStart.getDay())
lastWeekStart.setHours(0, 0, 0, 0);
const lastWeekEnd = new Date(lastWeekStart);
lastWeekEnd.setDate(lastWeekEnd.getDate() + 6)
lastWeekEnd.setHours(23, 59, 59, 999);
// 지난주에 진행된 투표 데이터를 조회
const lastWeekVotes = await this.votesRepository.find({
where: {
createdAt: Between(lastWeekStart, lastWeekEnd),
},
});
// 투표 기반으로 명예의 전당 집계
const hallOfFameData = this.aggVotesForHallOfFame(lastWeekVotes)
await this.updateHallOfFameDatabase(hallOfFameData);
}
// 날짜 추상화 매서드
private getThisMonthRange(){
const start = new Date();
start.setDate(1); // 이번달 첫쨰날
start.setHours(0, 0, 0, 0) // 자정
const end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
end.setHours(23, 59, 59, 999) // 하루의 마지막 시간
return { start, end }
}
// 투표 데이터 집계 매서드
private async aggVotesForHallOfFame(votes: Votes[]){
const { start, end } = this.getThisMonthRange();
const candidates = await this.votesRepository
.createQueryBuilder("vote")
.select(['vote.id', 'vote.title1', 'vote.title2'])
.addSelect("vote.voteCount1 + vote.voteCount2", "totalVotes")
.where('vote.createdAt BETWEEN :start AND :end', { start: start.toISOString(), end: end.toISOString() })
.having("totalVotes >= :minTotalVotes", { minTotalVotes: 100 }) // 투표 수 100 이상인 것만 조회
.orderBy('totalVotes', "DESC")
.limit(1000)// 1000개 이상의 데이터가 없어도 남은 데이터 만큼 올라간다. 즉 데이터 집계 상한선이 1000개 라는 뜻
.groupBy("vote.id")
.getRawMany();
return candidates
}
// DB에 명예의 전당 데이터를 업데이트(배열형태로 받아서 한번에 저장) ver 1.
private async updateHallOfFameDatabase(hallOfFameData: any){
// 한번에 저장
const newHallOfFameEntries = hallOfFameData.map(data => {
const newHallOfFameEntry = new TrialHallOfFames();
newHallOfFameEntry.id = data.id // vote table의 id임다
newHallOfFameEntry.userId = data.trial.userId // vote에는 userId가 없으므로 일대일관계인 trial에 가서 userId 가져옴
newHallOfFameEntry.title = data.title1 + 'Vs' + data.title2
newHallOfFameEntry.content = data.trial.content // vote에는 content가 없으므로 일대일관계인 trial에 가서 content 가져옴
newHallOfFameEntry.createdAt = new Date();
newHallOfFameEntry.updatedAt = new Date();
return newHallOfFameEntry;
});
// DB에 새로운 명전 저장
await this.trialHallOfFamesRepository.save(newHallOfFameEntries)
}
// DB에 명예의 전당 데이터를 업데이트(배열형태로 받아서 한번에 저장) ver 1.
private async updateHallOfFameDatabase(hallOfFameData: any){
// 한번에 저장
const newHallOfFameEntries = hallOfFameData.map(data => {
const newHallOfFameEntry = new TrialHallOfFames();
newHallOfFameEntry.id = data.id // vote table의 id임다
newHallOfFameEntry.userId = data.trial.userId // vote에는 userId가 없으므로 일대일관계인 trial에 가서 userId 가져옴
newHallOfFameEntry.title = data.title1 + 'Vs' + data.title2
newHallOfFameEntry.content = data.trial.content // vote에는 content가 없으므로 일대일관계인 trial에 가서 content 가져옴
newHallOfFameEntry.createdAt = new Date();
newHallOfFameEntry.updatedAt = new Date();
return newHallOfFameEntry;
});
// DB에 새로운 명전 저장
await this.trialHallOfFamesRepository.save(newHallOfFameEntries)
}
에 트랜잭션을 적용해 주었다.
private async updateHallOfFameDatabase(hallOfFameData: any) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 한 번에 저장하는 로직을 트랜잭션 내에서 실행
const newHallOfFameEntries = hallOfFameData.map(data => /* 데이터 매핑 로직 유지 */);
await queryRunner.manager.save(newHallOfFameEntries);
await queryRunner.commitTransaction(); // 업데이트 로직이 성공하면 커밋
} catch (err) {
await queryRunner.rollbackTransaction(); // 실패하면 롤백
throw err; // 에러를 다시 던져 상위 로직에서 처리할 수 있도록 함
} finally {
await queryRunner.release(); // 트랜잭션 종료
}
}```