2025년 3월 4일

김동환·2025년 3월 4일

TIL (Today I Learned)

게임 서버의 게임 종료 처리 (endGame 함수)

  • roomId를 기반으로 진행 중인 gameId를 가져와 게임 데이터 조회
  • 생존한 마피아와 시민 수를 계산하여 게임 종료 여부를 판단
  • 게임 종료 조건 충족 시 승리 팀(winningTeam) 결정
  • 최종 게임 상태(finalState)를 구성하여 각 플레이어의 역할 및 생존 여부 저장
  • 중복 저장을 방지하기 위해 gameResultKey 존재 여부 확인 후 Redis 트랜잭션 처리
  • Redis에 게임 결과를 저장한 후 gameResults 채널에 Pub/Sub 전송
  • 게임 종료 후 관련 게임 데이터를 Redis에서 삭제

API 서버의 게임 결과 구독 (GameResultsSubscriber)

  • gameResults 채널을 Redis에서 구독하여 게임 결과 수신
  • 게임 결과 수신 후 gameResultKey 존재 여부를 확인하여 중복 저장 방지
  • 게임 결과를 GameResultsService를 통해 RDS에 저장
  • 필요 시 Redis에서 gameResultKey 삭제 가능

API 서버의 게임 결과 저장 (GameResultsService)

  • gameIdwinningTeam을 기반으로 결과를 저장
  • 시민 팀에 속하는 역할(citizen, police, doctor)과 마피아를 비교하여 승패 계산
  • 각 플레이어별 승패(win 또는 lose)와 생존 여부를 GameResult 엔티티에 저장
  • 특정 게임 ID에 대한 결과 조회 기능 제공
  • 최근 10개의 게임 결과를 timestamp 기준으로 조회하는 기능 구현

느낀 점

  • Redis를 활용한 Pub/Sub 패턴을 적용하여 분산 서버에서 게임 결과를 효율적으로 전송할 수 있었다.
  • 중복 저장 방지를 위한 Redis exists 체크와 트랜잭션 처리를 통해 데이터 정합성을 유지했다.
  • 게임 결과를 API 서버에서 RDS에 저장하는 과정에서 역할에 따른 승패 계산 로직을 명확히 정의했다.
  • 전체적인 게임 서버와 API 서버의 역할을 분리하여 확장성과 유지보수성을 높였다.

분산 서버 중에서 게임 서버

 const gameResult = {
      roomId,
      gameId,
      winningTeam,
      finalState,
      timestamp: new Date().toISOString(),
    };

    const multi = this.redisClient.multi();
    multi.set(gameResultKey, JSON.stringify(gameResult), 'EX', 86400); // 24시간 유지
    multi.publish('gameResults', JSON.stringify(gameResult)); // Redis Pub/Sub 전송
    await multi.exec(); // 트랜잭션 실행

    console.log(`게임 결과가 저장됨 (gameId: ${gameId})`);

엔드 게임 로직에 퍼블리시 코드를 만들어 줬다.

그리고 api 서버에서 구독파일을 만들어서 받아준다.

@Injectable()
export class GameResultsSubscriber implements OnModuleInit {
  private redisSubscriber: Redis;
  private redisClient: Redis;

  constructor(private readonly gameResultsService: GameResultsService) {
    // 일반 Redis 클라이언트 (데이터 조회 용)
    this.redisClient = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: 6379,
    });

    // 구독 전용 Redis 클라이언트
    this.redisSubscriber = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: 6379,
    });
  }

  async onModuleInit() {
    console.log('게임 결과 Redis 구독 시작');

    this.redisSubscriber.subscribe('gameResults', (err, count) => {
      if (err) {
        console.error('Redis 구독 실패:', err);
      } else {
        console.log(`게임 결과 채널 구독 중... (${count})`);
      }
    });

    this.redisSubscriber.on('message', async (channel, message) => {
      if (channel === 'gameResults') {
        console.log(`게임 결과 수신: ${message}`);
        const gameResult = JSON.parse(message);
        const gameId = gameResult.gameId;
        const gameResultKey = `gameResult:${gameId}`;

        // 이미 저장된 게임 결과인지 확인 (중복 방지)
        const isAlreadyStored = await this.redisClient.exists(gameResultKey);
        if (!isAlreadyStored) {
          console.warn(`게임 결과가 Redis에 없음 (gameId: ${gameId}), 무시.`);
          return;
        }

        // 게임 결과 RDS에 저장
        await this.gameResultsService.saveGameResult(gameResult);
        console.log(`게임 결과가 RDS에 저장됨 (gameId: ${gameId}).`);

        // RDS에 저장 완료 후, Redis에서 해당 게임 결과 삭제 (필요 시)
        // await this.redisClient.del(gameResultKey);
      }
    });
  }
}

또한 게임 결과 서비스 로직에서 받은 값을 저장해주면 된다.

// 게임 결과 저장 (플레이어 개별 저장)
  async saveGameResult(gameData: any): Promise<void> {
    const gameId = gameData.gameId;
    const winningTeam = gameData.winningTeam;

    // 시민팀에 속하는 역할 목록 (게임 서버의 로직과 일치해야 함)
    const citizenRoles = ['citizen', 'police', 'doctor'];

    const results = gameData.finalState.players.map((player) => {
      // 플레이어가 이긴 팀에 속하는지 확인
      const isWinner =
        (winningTeam === 'citizens' && citizenRoles.includes(player.role)) ||
        (winningTeam === 'mafia' && player.role === 'mafia');

      return {
        gameId,
        userId: player.userId,
        role: player.role,
        alive: player.alive ? 'alive' : 'dead',
        winningTeam, // 올바르게 전달
        result: isWinner ? 'win' : 'lose', // 승패 계산 수정
      };
    });

    await this.gameResultRepository.save(results);
    console.log(` 게임 결과 저장 완료 (gameId: ${gameId})`);
  }

이렇게 만들면 분산 서버에서 pub/sub 를 사용할 수 있게 된다.

profile
Node.js 7기

0개의 댓글