Mini

24. 11. 19. 개발일지 // test-code 의존성 수정, redis 구독 리팩토링, 진행중게임에 들어올수없음 본문

개발일지

24. 11. 19. 개발일지 // test-code 의존성 수정, redis 구독 리팩토링, 진행중게임에 들어올수없음

Mini_96 2024. 11. 20. 01:27

* test-code 의존성 수정

  • 이슈
    • 기존의 Quiz도메인이 Quiz-set으로 변경
    • QuizService가 QuizSetService로 변경
    • QuizSetService내에서 QuizSetCreate, read, u, d 를 사용함
  • 만났던 에러
    • 해결 : QuizSetC r u d 를 providers에 주입해주니 해결되었음.

  • 각종 경로, 폴더명, 서비스명 수정

  • 서비스들을 export

  • 사용하는곳에서 모듈을 import 하면 위의 서비스들을 모두 사용할수 있다.

마이그레이션 후 test 성공

* 진행중인 게임방에 입장할수 없다.

  • startGame을 누르면 방의 상태를 바꾸는구나
  • 이걸 이용하면 되겠다.

  • createRoom할때 isWaiting을 1로 걸어주는데 이건뭐지?
  • 대기방을 찾는데 사용되는 정보같다.
  • 일단 둘다 or 조건으로 사용해보자.

  • 버그 : exception을 받았는데, joinRoom이 되버린것 같다.

  • client.join이 validation 후에 되야 될것같다.

  • 이제 event가 오지않는다.
  • client.join이 되고, 이벤트는 room에 대해 발생되는것같다.

  • 잘되는것같다. 휴.

  • was가 각각 room을 가지고 있는 형태인듯!

* test code

  • 트러블슈팅
  • 타임아웃이 발생하는원인 : 예외가 발생했는데, 응답을 못주고 있을 확률이 높다.

  • 먼저 Error Pipe Line을 살펴보자.
  • 음, 에러를 잡으면 exception event를 발생시키고 에러를 여기서 먹어버린다!!
  • 해결 : done을 활용해서 jest에게 명시적으로 테스트가 끝났다고 알려야 한다!!!

  • resolve는 Promise가 성공적으로 완료됐을 때 호출되고, 에러가 발생하면 reject가 호출됨.
  • WsExceptionFilter에서 에러를 잡고 종료하면 Promise의 resolve/reject가 호출되지 않음
  • 대신 Socket.IO의 'exception' 이벤트가 발생
  • 따라서 이벤트 리스너를 통해 테스트 완료를 처리해야 함
  • done 콜백이나 Promise와 이벤트 리스너를 결합하는 방식 사용 가능
  • 기존 테스트 코드 (실패):
// 비동기 완료를 Promise의 resolve로 처리하려 했음
it('게임 진행 중인 방 참여 실패', async () => {  
  const createResponse = await new Promise<{ gameId: string }>((resolve) => {
    client1.once(socketEvents.CREATE_ROOM, resolve);
    client1.emit(socketEvents.CREATE_ROOM, {...});
  });

  // ... 중략 ...

  // WsExceptionFilter에서 에러를 catch하고 끝나버려서
  // 이 Promise는 영원히 resolve되지 않음
  return new Promise<void>((resolve) => {
    client2.once('exception', (error) => {
      expect(error.event).toBe('joinRoom');
      resolve();
    });
    client2.emit(socketEvents.JOIN_ROOM, {...});
  });
});
  • done 콜백 사용 (성공):
// Jest에게 비동기 테스트의 완료 시점을 알려주는 done 콜백 사용
it('게임 진행 중인 방 참여 실패', (done) => {  
  client2.once('exception', (error) => {
    try {
      expect(error.event).toBe('joinRoom');
      done();  // 테스트 완료를 Jest에게 알림
    } catch (err) {
      done(err);  // 테스트 실패를 Jest에게 알림
    }
  });

  client1.once(socketEvents.CREATE_ROOM, (response) => {
    const gameId = response.gameId;
    client1.emit(socketEvents.JOIN_ROOM, {...});
  });

  client1.emit(socketEvents.CREATE_ROOM, {...});
});

차이점:

  1. async/await 방식:
    • Promise의 resolve를 기다림
    • WsExceptionFilter가 에러를 잡아버려서 Promise가 resolve되지 않음
    • 결과적으로 timeout 발생
  2. done 콜백 방식:
    • Jest에게 직접 테스트 완료를 알림
    • WsExceptionFilter의 에러 처리와 무관하게 동작
    • Socket.IO 이벤트 기반으로 테스트 완료 처리

간단히 말하면:

  • 기존에는 Promise가 resolve되길 기다렸는데, 에러가 필터에서 잡혀버려서 영원히 기다리게 됨
  • done을 사용하면 이벤트 핸들러에서 직접 테스트 완료를 알릴 수 있음

Jest의 테스트는 세 가지 방식으로 비동기 처리가 가능:

// 1. done 콜백
it('test1', (done) => {
  someAsyncOperation(() => {
    done();
  });
});

// 2. Promise return
it('test2', () => {
  return somePromiseOperation();
});

// 3. async/await
it('test3', async () => {
  await somePromiseOperation();
});

이 경우는 Socket.IO의 이벤트 기반 특성 때문에 done 콜백이 가장 적합했던 것입니다.

  • 결론
  • try문에서 exception 이벤트발생 -> 검증 -> done으로 test 명시적 종료
  • 위와 같은 흐름으로 바꾸면 되는것이다.

  • 에러와 jest에 대해 지식이 늘었다.

* redis 구독 리팩토링

  • 설계

  • 의존성 주입 error
  • 원인 : subscriber들의 필드인 Logger도 주입해줘야함
  • 해결 : 부모의 필드에서 1회만 생성 (각각 자식의 이름으로)

  • test code, 게임모듈 에도 의존성 추가

  • test code 통과

  • postman test 완료

  • 테스트 코드로 인해 리팩토링해도 겁먹을 일이 없다!!

* subscriber 들이 하는일 알아보기

flowchart TB
    R[Redis Events] --> S[ScoringSubscriber]
    R --> T[TimerSubscriber]
    R --> RO[RoomSubscriber]
    R --> P[PlayerSubscriber]

    S --> |"scoring:*"| SC[채점 결과 처리]
    T --> |"Room:*:timer"| TI[타이머 만료 처리]
    RO --> |"Room:*"| ROO[방 상태 변경 처리]
    P --> |"Player:*"| PL[플레이어 상태 변경 처리]

    style R fill:#f96,stroke:#333,stroke-width:4px

 

  1. ScoringSubscriber: 채점 이벤트 구독 ('scoring:*')
  • 모든 플레이어의 답안 제출 완료 시 채점 결과 처리
  • 점수 집계 및 리더보드 업데이트
  • 채점 결과를 클라이언트들에게 전송
  1. TimerSubscriber: 타이머 만료 이벤트 구독 ('Room:*:timer')
  • 퀴즈 시작/종료 타이밍 제어
  • 퀴즈 시작 시: 다음 문제 전송
  • 퀴즈 종료 시: 답안 제출 시간 종료 및 채점 시작
  • 마지막 문제인 경우 게임 종료 처리
  1. RoomSubscriber: 방 상태 변경 이벤트 구독 ('Room:*')
  • 방 옵션 변경 시 업데이트 ('Option')
  • 퀴즈셋 변경 시 업데이트 ('Quizset')
  • 게임 시작 시 처리 ('Start')
  • 변경사항을 해당 방의 모든 클라이언트에게 전송
  1. PlayerSubscriber: 플레이어 상태 변경 이벤트 구독 ('Player:*')
  • 새 플레이어 입장 처리 ('Join')
  • 플레이어 위치 업데이트 ('Position')
  • 플레이어 퇴장 처리 ('Disconnect')
  • 변경사항을 게임방의 모든 클라이언트에게 전송

각 Subscriber의 주요 흐름:

1. Redis 이벤트 구독 (psubscribe)
2. 이벤트 발생 시 처리 (on 'pmessage')
3. 데이터 검증 및 처리
4. Socket.IO를 통해 클라이언트에게 결과 전송

예시로 TimerSubscriber의 주요 흐름:

1. 타이머 만료 발생
2. 현재 퀴즈 상태 확인
3. 'start' 상태면:
   - 답안 제출 시간 종료
   - 채점 진행
   - 결과 전송
4. 'end' 상태면:
   - 다음 문제 준비
   - 새로운 타이머 설정
   - 문제 전송

이런 구조로 인해:

  1. 관심사 분리가 명확함
  2. 각 기능의 독립적인 확장/수정이 용이함
  3. 코드 유지보수가 쉬움
  4. 테스트가 용이함

 

  • 전체 data 흐름

게임방생성
3,4번 퀴즈를 풀겠다는 뜻인가?

# 1. 게임방 생성 시점의 Redis 데이터
HGETALL Room:123
1) "host"            "socket1"
2) "status"          "waiting"
3) "title"           "Test Room"
4) "gameMode"        "RANKING"
5) "maxPlayerCount"  "5"
6) "isPublicGame"    "1"
7) "isWaiting"       "1"
8) "quizSetId"       "1"
9) "quizCount"       "3"

# 2. 게임 시작 시 퀴즈 설정
SMEMBERS Room:123:QuizSet
1) "quiz1"
2) "quiz2"
3) "quiz3"

HGETALL Room:123:Quiz:quiz1
1) "quiz"      "1+1=?"
2) "answer"    "2"
3) "limitTime" "30"

# 3. 첫 번째 퀴즈 시작
GET Room:123:CurrentQuiz
"0:start"

# 타이머 설정 (30초)
SET Room:123:timer "timer" EX 30
OK

# 이 시점에 타이머가 동작 중이며, 30초 후 만료되면:
# "__keyspace@0__:Room:123:timer" -> "expired" 이벤트 발생

# 4. 플레이어들의 위치 정보 (답안 선택)
HGETALL Player:socket1
1) "playerName"  "Player1"
2) "positionX"   "0.2"    # 왼쪽 위치 = 1번 답안
3) "positionY"   "0.3"    # 위쪽 위치
4) "gameId"      "123"

HGETALL Player:socket2
1) "playerName"  "Player2"
2) "positionX"   "0.8"    # 오른쪽 위치 = 2번 답안
3) "positionY"   "0.2"    # 위쪽 위치
4) "gameId"      "123"

# 5. 타이머 만료 후 채점 결과
HGET Player:socket1 isAnswerCorrect
"1"  # 정답

HGET Player:socket2 isAnswerCorrect
"0"  # 오답

# 6. 리더보드 점수 업데이트
ZRANGE Room:123:Leaderboard 0 -1 WITHSCORES
1) "socket1"
2) "1000"     # 혼자 맞춰서 1000점
3) "socket2"
4) "0"

# 7. 다음 퀴즈로 진행
GET Room:123:CurrentQuiz
"0:end"

# 새로운 타이머 설정 (10초 대기)
SET Room:123:timer "timer" EX 10
OK

# 10초 후 다음 퀴즈 시작
GET Room:123:CurrentQuiz
"1:start"

SET Room:123:timer "timer" EX 30
OK

# 이런 식으로 반복...
  • __keyspace@0__:Room:* 란?