* test-code 의존성 수정
- 이슈
- 기존의 Quiz도메인이 Quiz-set으로 변경
- QuizService가 QuizSetService로 변경
- QuizSetService내에서 QuizSetCreate, read, u, d 를 사용함
- 만났던 에러
- 해결 : QuizSetC r u d 를 providers에 주입해주니 해결되었음.
- 각종 경로, 폴더명, 서비스명 수정
- 서비스들을 export
- 사용하는곳에서 모듈을 import 하면 위의 서비스들을 모두 사용할수 있다.
* 진행중인 게임방에 입장할수 없다.
- 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, {...});
});
차이점:
async/await
방식:- Promise의 resolve를 기다림
- WsExceptionFilter가 에러를 잡아버려서 Promise가 resolve되지 않음
- 결과적으로 timeout 발생
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
- ScoringSubscriber: 채점 이벤트 구독 ('scoring:*')
- 모든 플레이어의 답안 제출 완료 시 채점 결과 처리
- 점수 집계 및 리더보드 업데이트
- 채점 결과를 클라이언트들에게 전송
- TimerSubscriber: 타이머 만료 이벤트 구독 ('Room:*:timer')
- 퀴즈 시작/종료 타이밍 제어
- 퀴즈 시작 시: 다음 문제 전송
- 퀴즈 종료 시: 답안 제출 시간 종료 및 채점 시작
- 마지막 문제인 경우 게임 종료 처리
- RoomSubscriber: 방 상태 변경 이벤트 구독 ('Room:*')
- 방 옵션 변경 시 업데이트 ('Option')
- 퀴즈셋 변경 시 업데이트 ('Quizset')
- 게임 시작 시 처리 ('Start')
- 변경사항을 해당 방의 모든 클라이언트에게 전송
- PlayerSubscriber: 플레이어 상태 변경 이벤트 구독 ('Player:*')
- 새 플레이어 입장 처리 ('Join')
- 플레이어 위치 업데이트 ('Position')
- 플레이어 퇴장 처리 ('Disconnect')
- 변경사항을 게임방의 모든 클라이언트에게 전송
각 Subscriber의 주요 흐름:
1. Redis 이벤트 구독 (psubscribe)
2. 이벤트 발생 시 처리 (on 'pmessage')
3. 데이터 검증 및 처리
4. Socket.IO를 통해 클라이언트에게 결과 전송
예시로 TimerSubscriber의 주요 흐름:
1. 타이머 만료 발생
2. 현재 퀴즈 상태 확인
3. 'start' 상태면:
- 답안 제출 시간 종료
- 채점 진행
- 결과 전송
4. 'end' 상태면:
- 다음 문제 준비
- 새로운 타이머 설정
- 문제 전송
이런 구조로 인해:
- 관심사 분리가 명확함
- 각 기능의 독립적인 확장/수정이 용이함
- 코드 유지보수가 쉬움
- 테스트가 용이함
- 전체 data 흐름
# 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:* 란?
'개발일지' 카테고리의 다른 글
24. 11. 21. 개발일지 // 자동배포 수정, 인터셉터 버그수정(redis event trigger 이해) (0) | 2024.11.22 |
---|---|
24. 11. 20. 개발일지 // redis 메모리 관리 (0) | 2024.11.21 |
24. 11. 18. 개발일지 // api vs 서비스주입, redis 캐시 (1) | 2024.11.19 |
24. 11. 17. 개발일지 redis 탐색 (0) | 2024.11.18 |
24.11.14. 발표자료 // n+1문제해결, 변경감지, redis 설계 (0) | 2024.11.14 |