Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat#205 실제 퀴즈셋 가져오기 #213

Merged
merged 8 commits into from
Nov 19, 2024
1 change: 1 addition & 0 deletions BE/src/common/constants/redis-key.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const REDIS_KEY = {
ROOM_TIMER: (gameId: string) => `Room:${gameId}:Timer`,
ROOM_QUIZ_SET: (gameId: string) => `Room:${gameId}:QuizSet`,
PLAYER: (playerId: string) => `Player:${playerId}`,
QUIZSET_ID: (quizSetId: number) => `Quizset:${quizSetId}`,

ACTIVE_ROOMS: 'ActiveRooms' // 활성화된 방 목록을 저장하는 Set (핀번호 중복 체크하기 위함)
};
14 changes: 11 additions & 3 deletions BE/src/game/game.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ import { GameGateway } from './game.gateway';
import { GameService } from './service/game.service';
import { RedisModule } from '@nestjs-modules/ioredis';
import { GameValidator } from './validations/game.validator';
import { HttpModule } from '@nestjs/axios';
import { GameChatService } from './service/game.chat.service';
import { GameRoomService } from './service/game.room.service';
import { QuizModule } from '../quiz/quiz.module';
import { QuizCacheService } from './service/quiz.cache.service';

@Module({
imports: [RedisModule, HttpModule],
providers: [GameGateway, GameService, GameChatService, GameRoomService, GameValidator]
imports: [RedisModule, QuizModule],
providers: [
GameGateway,
GameService,
GameChatService,
GameRoomService,
GameValidator,
QuizCacheService
]
})
export class GameModule {}
6 changes: 4 additions & 2 deletions BE/src/game/service/game.room.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ export class GameRoomService {
isPublicGame: gameConfig.isPublicGame ? '1' : '0',
isWaiting: '1',
lastActivityAt: new Date().getTime().toString(),
quizSetId: '0',
quizCount: '2'
quizSetId: '-1', // 미설정시 기본퀴즈를 진행, -1은 기본 퀴즈셋
quizCount: '2',
quizSetTitle: '기본 퀴즈셋'
//todo : 기본 퀴즈셋 title 설정
});

await this.redis.sadd(REDIS_KEY.ACTIVE_ROOMS, roomId);
Expand Down
27 changes: 16 additions & 11 deletions BE/src/game/service/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { GameValidator } from '../validations/game.validator';
import SocketEvents from '../../common/constants/socket-events';
import { StartGameDto } from '../dto/start-game.dto';
import { Server } from 'socket.io';
import { HttpService } from '@nestjs/axios';
import { mockQuizData } from '../../../test/mocks/quiz-data.mock';
import { QuizCacheService } from './quiz.cache.service';

@Injectable()
export class GameService {
Expand All @@ -17,8 +17,8 @@ export class GameService {

constructor(
@InjectRedis() private readonly redis: Redis,
private readonly httpService: HttpService,
private readonly gameValidator: GameValidator
private readonly gameValidator: GameValidator,
private readonly quizCacheService: QuizCacheService
) {}

async updatePosition(updatePosition: UpdatePositionDto, clientId: string) {
Expand Down Expand Up @@ -49,14 +49,19 @@ export class GameService {

this.gameValidator.validatePlayerIsHost(SocketEvents.START_GAME, room, clientId);

// const getQuizsetURL = `http://localhost:3000/api/quizset/${room.quizSetId}`;
//
// // REFACTOR: get 대신 Promise를 반환하는 axiosRef를 사용했으나 더 나은 방식이 있는지 확인
// const quiz-set = await this.httpService.axiosRef({
// url: getQuizsetURL,
// method: 'GET'
// });
const quizset = mockQuizData;
/**
* 퀴즈셋이 설정되어 있지 않으면 기본 퀴즈셋을 사용
*/
const quizset =
room.quizSetId === '-1'
? mockQuizData
: await this.quizCacheService.getQuizSet(+room.quizSetId);
Comment on lines +52 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: 디폴트 퀴즈셋 -1을 게임방 만들 때부터 설정해두는 건 어떨까요? 그러면 아래의 경우에 quizSetTitle을 넣을 수 있게 됩니다.

  1. 대기방이 만들어지면서 디폴트 퀴즈셋이 정해질 때
  2. 대기방에서 호스트에 의해 퀴즈셋이 변경될 때

그래서 이렇게 게임이 시작되기 전에 quizSetTitle 관리를 해줘야 대기방 목록 조회할 때 퀴즈셋 제목까지 정확히 보여줄 수 있습니다!


//roomKey에 해당하는 room에 quizSetTitle을 quizset.title로 설정
await this.redis.hset(roomKey, {
quizSetTitle: quizset.title
});

this.gameValidator.validateQuizsetCount(
SocketEvents.START_GAME,
parseInt(room.quizCount),
Expand Down
79 changes: 79 additions & 0 deletions BE/src/game/service/quiz.cache.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { QuizSetData } from '../../InitDB/InitDB.Service';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import { Redis } from 'ioredis';
import { QuizService } from '../../quiz/quiz.service';
import { mockQuizData } from '../../../test/mocks/quiz-data.mock';
import { REDIS_KEY } from '../../common/constants/redis-key.constant';

@Injectable()
export class QuizCacheService {
Comment on lines +9 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 아래처럼 각 방마다 퀴즈에 대한 정보가 저장되고 있습니다. 즉, 하나의 퀴즈셋이 메모리에 중복해서 저장될 수 있습니다.

# 퀴즈셋 관련 (quizId는 DB quiz 테이블에 저장되어 있는 기본키)
Room:<gameId>:Quiz:<quizId> → Hash
- quiz: "퀴즈 내용"
- answer: "1"
- limitTime: "10"
- choiceCount: "4"

만약 캐시를 본격적으로 도입한다면 아래 정보는 삭제하는 방향으로 가고, 퀴즈 진행 로직 관련해서 캐시된 데이터를 활용하는 걸로 수정해야 메모리를 효율적으로 쓸 수 있을 것 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
db 대용으로 data를 통쨰로 저장하는 중입니다.

private readonly quizCache = new Map<string, any>();
private readonly logger = new Logger(QuizCacheService.name);
private readonly CACHE_TTL = 1000 * 60 * 30; // 30분
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

캐시 만료를 30분으로 잡아주신 이유가 있을까요? (궁금)


constructor(
@InjectRedis() private readonly redis: Redis,
private readonly quizService: QuizService
) {}

/**
* Redis 캐시에서 퀴즈셋 조회
*/
private async getFromRedisCache(quizSetId: number) {
const cacheKey = REDIS_KEY.QUIZSET_ID(quizSetId);
const cachedData = await this.redis.get(cacheKey);

if (cachedData) {
return JSON.parse(cachedData);
}
return null;
}

/**
* Redis 캐시에 퀴즈셋 저장
*/
private async setToRedisCache(quizSetId: number, data: QuizSetData): Promise<void> {
const cacheKey = REDIS_KEY.QUIZSET_ID(quizSetId);
await this.redis.set(cacheKey, JSON.stringify(data), 'EX', this.CACHE_TTL);
}

/**
* 퀴즈셋 데이터 조회 (캐시 활용)
*/
async getQuizSet(quizSetId: number) {
// 1. 로컬 메모리 캐시 확인
// const localCached = this.quizCache.get(this.getCacheKey(quizSetId));
// if (localCached) {
// this.logger.debug(`Quiz ${quizSetId} found in local cache`);
// return localCached;
// }
Comment on lines +45 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 주석처리해두신 이유가 뭘까요?


// 2. Redis 캐시 확인
const redisCached = await this.getFromRedisCache(quizSetId);
if (redisCached) {
this.logger.debug(`Quiz ${quizSetId} found in Redis cache`);
// 로컬 캐시에도 저장
// this.quizCache.set(REDIS_KEY.QUIZSET_ID(quizSetId), redisCached);
return redisCached;
}

// 3. DB에서 조회
const quizData = quizSetId === -1 ? mockQuizData : await this.quizService.findOne(quizSetId);

// 4. 캐시에 저장
await this.setToRedisCache(quizSetId, quizData);
this.quizCache.set(REDIS_KEY.QUIZSET_ID(quizSetId), quizData);

return quizData;
}

/**
* 캐시 무효화
*/
async invalidateCache(quizSetId: number): Promise<void> {
const cacheKey = REDIS_KEY.QUIZSET_ID(quizSetId);
this.quizCache.delete(cacheKey);
await this.redis.del(cacheKey);
}
}
Loading