-
[TIL] 타워 디펜스 팀 프로젝트 종료 (D-Day)TIL-sparta 2024. 6. 21. 21:16
학습 키워드: Redis, Node.js. express, socket.io, transaction, concurrency
1. 완료한 과제 진행 사항
요약) 버그 수정 작업:
- 클라이언트에서 타워 환불/업그레이드 이벤트를 잘 못 송신하는 케이스를 수정
- 서버 내 유저 게임 데이터의 스테이지 정보가 제대로 업데이트 되지 않던 문제를 수정
-> 검증 로직 자체에는 문제가 없었으나 아래에 작성한 redis 동시성 문제로 인한 anomaly 발생 때문에 정보의 불일치가 생겨 검증에 실패하는 경우가 있었음
2. 과제 진행 간 문제점
Redis 데이터 수정 시 동시성 문제 발생:
과제 진행 초반에 작성하고 지속적으로 수정하던 redis 데이터 수정용 함수들이 있는데, 제출 전날과 당일에 기능을 테스트하면서 고레벨 타워를 몬스터 소환 위치에 다수 배치하고 확인하던 중 못 보던 에러들이 출력되는 것을 확인했다. 원인 파악 후 깨닫게된 사실은 타워 뿐만이 아니라 몬스터 처치, 골드 획득, 스테이지 이동 등의 이벤트가 동시에 같은 유저의 게임 데이터를 수정하게 되는 상황이 자주 발생하기 때문에 여러 비동기 함수들이 동시에 데이터를 읽어오고 다시 쓰는 과정에서 anomaly가 발생하게 된다는 것이었다.
이전 TIL에서 언급되었듯이 3일만에 기능 구현을 완료해야해서 시간이 촉박하다고 판단하고 문제의 해결을 위해 유저 게임 데이터의 타워 목록만 따로 분리하여 관리하고 있었는데, 문제는 같은 방식으로 해결하려면 10개 가량의 field 값을 모두 분리해줘야 한다는 것이었다. 그래서 이 방법을 사용할 바에 미뤄뒀던 transaction 적용을 진행하는 것이 맞겠다고 판단했다.
... /** * 유저의 게임 데이터 생성 * @param {uuid} uuid 유저의 UUID * @param {number} gold 유저 보유 gold * @param {number} stageId 현재 스테이지 ID * @param {number} score 유저 점수 * @param {number} numOfInitialTowers 초기 타워 개수 * @param {number} baseHp 기지의 HP * @param {number} startTime 게임 시작 시간(클라이언트 기준, Date.now()) * @param {number} spawnInterval 몬스터 소환 interval in millis * @param {number} lastGoblinSpawnTime 마지막 고블린 스폰 시간 (클라 기준, Date.now()) * @param {number} killCount 몬스터 킬 카운트 * @param {number} goblinKillCount 고블린 킬 카운트 */ createGameData: async function (uuid, gold, stageId, score, numOfInitialTowers, baseHp, startTime, spawnInterval, lastGoblinSpawnTime, killCount, goblinKillCount) { try { const key = `${GAME_DATA_PREFIX}${uuid}`; const data = await redisClient.hVals(key); await redisClient.watch(key); const transaction = redisClient.multi(); if (!data || data.length === 0) { transaction.hSet(key, `${GAME_FIELD_GOLD}`, `${gold}`); transaction.hSet(key, `${GAME_FIELD_STAGE}`, `${stageId}`); transaction.hSet(key, `${GAME_FIELD_SCORE}`, `${score}`); transaction.hSet(key, `${GAME_FIELD_INITIAL_TOWERS}`, `${numOfInitialTowers}`); transaction.hSet(key, `${GAME_FIELD_BASE_HP}`, `${baseHp}`); transaction.hSet(key, `${GAME_FIELD_START_TIME}`, `${startTime}`); transaction.hSet(key, `${GAME_FIELD_SPAWN_INTERVAL}`, `${spawnInterval}`); transaction.hSet(key, `${GAME_FIELD_GOBLIN_TIME}`, `${lastGoblinSpawnTime}`); transaction.hSet(key, `${GAME_FIELD_KILL_COUNT}`, `${killCount}`); transaction.hSet(key, `${GAME_FIELD_GOBLIN_KILL_COUNT}`, `${goblinKillCount}`); while (true) { const result = await transaction.exec(); if (result) { console.log('createGameData successful.'); break; } } } } catch (err) { console.error('Error creating game data: ', err); } finally { await redisClient.unwatch(); } }, ...
위의 코드 블럭은 프로젝트에 적용된 redis 데이터 접근용 함수 중 하나다. 먼저 watch(key)를 사용하여 key를 감시하는데, 이렇게 하면 unwatch가 실행되기 전까지 한 번의 exec 또는 transaction 외의 명령만 실행할 수 있다고 한다. 이후 transaction에 multi() 를 사용하여 시작하며, 해당 transaction에 적용되는 redis 명령은 바로 실행되지 않고 queue에 쌓이게 된다. 이렇게 쌓인 명령을 최종적으로 exec()을 호출하여 일괄 실행할 수 있다. 다만 명령을 허가하지 않는다는 이유 때문인지 게임 시작 시 실행하는 createGameData 함수의 transaction이 진행되는 동안 patchGameDataTower 함수의 명령이 취소되어 타워 정보가 불일치하게 되는 현상이 있어 해당 코드들에 대해 while 루프와 retry 카운터, delay 등 (delay는 한 번 실패할 때 마다 * 2 되어 재시도 간격이 늘어나도록 설정, 기본 100ms)을 적용하여 3번 까지 명령을 재실행할 수 있도록 설정했더니 문제가 해결됐다. 위의 코드가 가장 먼저 작성된 코드라 당시 고려하지 않았던 retry 횟수가 적용되어 있지 않다. 추후 프로젝트에서 redis를 사용하게 될 때 좀 더 다듬어진 코드를 작성하도록 해봐야겠다.
3. 피드백 이후 (추가)hSet으로 저장한 유저 데이터의 field들을 별개의 key로 분리하기:
튜터님 피드백을 읽고나서 생각한 점은 아무래도 transaction 적용 시 retry를 위해 while 루프를 돌리는 등 리소스 소모가 늘어나게 되는데, 유저 수가 적은 게임이라면 괜찮겠지만 다수의 유저가 사용하는 멀티 플레이 게임에서 이런 식의 코드가 적용되면 그만큼 서버의 부담이 매우 커진다. 따라서 트랜잭션을 적용하는 대신 이전 TIL에서 짧게 언급했던 유저 타워 목록의 분리처럼 유저의 게임 데이터에 hSet으로 추가했던 모든 field들을 별개의 key에 저장하여 각각 따로 업데이트 되도록 설계하는 식으로 같은 key를 수정하는 상황을 최소화 하는 것이 유리하다는 뜻이 되겠다.
--
REFERENCES:> 팀 프로젝트 repo
> "[redis] 트랜잭션(Transaction) - 이론편" by 사바라다
> "[database] 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)" by 사바라다
> "redis transaction" by Joker
728x90'TIL-sparta' 카테고리의 다른 글
프로그래머스) 그룹별 조건에 맞는 식당 출력하기 풀이 (MySQL) (0) 2024.06.23 프로그래머스) 대여 횟수가 많은 자동차들의 월별 대여 횟수 구하기 풀이 (MySQL) (0) 2024.06.22 강의 과제) 응용 계층, DNS, HTTP (2) 2024.06.20 [TIL] 타워 디펜스 팀 프로젝트 진행 (D-2) (0) 2024.06.19 [TIL] 타워 디펜스 팀 프로젝트 진행 (D-3) (0) 2024.06.19