효과는 굉장했다!
이번 실전 프로젝트 제목이자 팀이름이다. 이번 프로젝트의 분야가 텍스트로 이루어진 커맨드라인 게임이자 MUD게임의 한 종류를 하는 것과 나름대로 팀원분이 제시한 내용에 유쾌함도 느껴져 모두 웃으면서 찬성하게 되었다.
게임이라는 주제가 단순히 생각하면 재밌겠다는 생각과 배울 것이 많을 것 같다는 생각이 들었고, 실제로도 매우 복잡한 연산과 탄탄한 알고리즘이 뒷받침이 따라주어야 한다는 사실을 깨달았을 때는 이미 늦은 순간이었다.
하지만 Node.js의 장점을 살리면서, 백엔드반의 취지에 맞는 데이터 핸들링을 해내기엔 이만한 주제가 없다고도 생각했고 다른 팀원분들도 너무 열심히 해주셔서 어쩌면 덩달아 나까지도 뒤처지지 않고 민폐가 되지 않게 더욱 열심히 하려고 덤빈 것 같다.
게임 전투방식 중 몬스터를 만나게 되었을 때 DB상에 몬스터가 추가되는데, 필드(던전) 난이도에 비례하여 몬스터의 이름과 능력치가 다르게 생성되게 구현하기 위해 기초가되는 몬스터의 디폴트를 정하고, 난이도와 등장 확률마다 일반, 정예, 보스급 몬스터를 구현하기위해 코드를 작성하려던 와중 뽑기 요소처럼 확률을 정해놓고 그것이 나오게 하는 코드를 짜본 적이 없었지만, 팀원분들과 구글링의 도움으로 해결하게 되었다.
- 몬스터 생성 코드
/***************************************************************
* 확률에 따른 몬스터 등급 정하는 함수
***************************************************************/
static isRere() {
// 랜덤값 생성(1~100)
const ranNum: number = Math.floor(Math.random() * 99 + 1);
// 몬스터의 타입을 결정, 일반, 정예, 보스 순
const type: number[] = [2, 1, 0];
// 각 희귀도에 따른 등장확률
const isRere: number[] = [2, 28, 70];
let res: number;
for (let i = 0; i < type.length; i++) {
if (isRere[i] >= ranNum) {
res = type[i];
return res;
} else if (isRere[isRere.length - 1] < ranNum) {
res = type[type.length - 1];
return res;
}
}
}
/***************************************************************
* 해당 던전의 몬스터 생성
***************************************************************/
static async createMonster(fieldId: number, characterId: number) {
// 여기에 일반, 희귀, 보스 몬스터를 결정할 확률을 만드는 코드를 만들자.
// 0이 나올 확률은 80, 1은 15, 2는 5
// 기본적으로 monsterId 1, 2, 3 은 첫 던전의 일반 3가지 몬스터
// 경험치 획득량은 캐릭터 필요 경험치랑 참고하자.
// 확률적으로 1이 나오면 이름 앞에 '정예'를 넣어주고 각 능력치가 1.5배
// 2가 나오면 이름앞에 '보스'를 넣어주고 각 능력치가 3배
const first: string[] = ['다람쥐', '고슴도치', '늑대'];
const second: string[] = ['고슴도치', '고블린', '고블린 대장'];
const therd: string[] = ['고블린', '오크', '오크 대장'];
const fourth: string[] = ['오크', '도적', '도적 대장'];
const fifth: string[] = ['도적', '좀비', '좀비 숙주'];
const sixth: string[] = ['좀비', '구울', '리치'];
const seventh: string[] = ['구울', '임프', '데몬 임프'];
const eighth: string[] = ['임프', '머미', '디아블로'];
const ninth: string[] = ['머미', '리퍼', '메피스토'];
const tenth: string[] = ['리퍼', '뱀파이어', '바알'];
const names = [
'뮤츠', // 원래는 null을 넣고싶었지만 에러가 나와 재미요소로 넣었다.
first,
second,
therd,
fourth,
fifth,
sixth,
seventh,
eighth,
ninth,
tenth,
];
let name: string;
let ratio: number;
let type = this.isRere();
if (type === 0) {
name = names[fieldId][0];
ratio = fieldId * 1;
}
if (type === 1) {
name = names[fieldId][1];
ratio = fieldId * 1.5;
}
if (type === 2) {
name = names[fieldId][2];
ratio = fieldId * 3;
}
const defaultMonster = {
hp: 50,
attack: 5,
defense: 5,
exp: 10,
};
if (!type) type = 0;
const dumyMonsters: MonsterInputForm = {
characterId,
fieldId,
type,
name: name!,
hp: Math.ceil(defaultMonster.hp * ratio!),
attack: Math.ceil(defaultMonster.attack * ratio!),
defense: Math.ceil(defaultMonster.defense * ratio!),
exp: Math.ceil(defaultMonster.exp * ratio!),
};
return await Monsters.create(dumyMonsters);
}
부하테스트, 그리고 artillery와 socket.io
본래 express와 REST API를 통한 게임을 진행하다가, 채팅기능은 socket.io를 이용하여 구현하였는데, 중간에 전투기능을 구현하던 와중 전부 socket.io로 진행하게 되었고, 나는 부하테스트를 맡게 되었다.
멘토님께서 추천해 주신 locust를 처음으로 도전해 보았지만, 도저히 첫 연결부터 되지 않고 연결이 끊어졌다는 에러만 나오고 그다음 시나리오에 대해 진행조차 되지 않았다.
[2022-11-13 21:43:39,719] JangKroed/ERROR/locust.user.task: Connection to remote host was lost.
Traceback (most recent call last):
raise WebSocketConnectionClosedException(
websocket._exceptions.WebSocketConnectionClosedException: Connection to remote host was lost.
멘토님께서 추천해주시기도 했고, 알아본 바로는 추후에 우리가 추가기능으로 생각하였던 MSA의 분산부하 테스트가 가능하다고 하여 조금 무리하면서 까지 알아보았지만 결국 더 이상 시간을 썼다가는 아무것도 얻지 못하고 팀원들에게 민폐가 될 것 같고, 꼭 locust만이 정답은 아닐 거라 생각하여 node.js와 호환성이 높다고 알려진 artillery로 다시 도전해 보았다.
이제까지 locust는 파이썬 코드를 이용하여 시나리오를 진행하였지만, artillery 같은 경우 앞서 swagger와 github action을 통해 경험해 보았던 yml 파일을 이용하여 시나리오를 작성하게 되어 조금 익숙하기도 하였고, 공식문서를 참고하면서 해본 결과 locust에 매달린 게 약간 허무하다고 생각될 정도로 연결과 시나리오 진행이 잘되었다.
물론 시나리오 과정에서 진행되거나 안 되는 빈도 차이가 발생하여 순서가 중요한 시나리오에 대한 처리가 필요한 코드에서 에러가 진행되었지만, 단순 아직 첫 발걸음을 뗀 만큼 이 부분은 앞으로 공식문서를 참고하며 진행해 볼 생각이다.
앞으로 중요한 과제
- docker, dockerhub
- kubernetes
- MSA
- CI / CD
- 부하테스트