12월 23일부터 1월 2일까지 진행한 팀 프로젝트 '타워 디펜스' 도중 있었던 문제 상황에 대한 트러블 슈팅을 해보려고 한다.
이번 프로젝트도 지난 RealTime 프로젝트와 동일하게 서버와 클라이언트의 통신이 주된 웹소켓 프로젝트였기에 진행하는 동안 많은 이슈들이 있었고 이번에는 그 중에서도 특히 '패킷 핸들러 교차 오류' 에 대해 이야기 해보려고 한다.
정확한 상황을 설명하자면 서버와 클라이언트가 소켓을 통해 데이터를 주고 받던 중, 요청을 보낸 위치에 정확하게 데이터가 전달되지 않고 다른 위치에서 응답을 받는 상황이 나타난 것이다.
이 SendEvent 함수는 클라이언트 측에서 socket.emit 을 통해 서버에게 데이터를 요청하는 함수이다.
데이터를 요청하는 위치에서는
다음과 같이 sendEvent 의 매개변수로 HandlerId 와 요청 처리에 필요한 payload 데이터를 함께 보낸다.
해서 정상적인 통신이 이루어졌다면 아래와 같이 message, userName, highScore, time 에 대응하는 값들이 response를 통해 할당되어야 맞겠지만, 문제는 이 sendEvent를 다양한 위치에서 사용하고 있다는 점에서 일어났다.
실제로 문제가 일어난 코드는 아니지만 예시를 위해 가져온 사진이다.
이처럼 클라이언트의 다양한 위치에서 서버와의 통신을 위해 sendEvent를 활용하고 있고, 그에 맞는 결과값을 받기를 대기하고 있다.
분명 3번 핸들러와 11번 핸들러는 다른 요청, 다른 데이터를 전달하고 그에 맞는 핸들러들 또한 다른 결과값을 전달해 우리는 각각 요청에 맞는 값이 할당될 것을 기대하지만 버그 상황은 11번 핸들러에 대한 응답이 위 response 에 할당되는 등의 과정이 일어난 것이다.
프로젝트가 웹소켓 통신을 기반으로 하고 있는 만큼 전반적인 로직이 모두 소켓 통신을 통해 이루어지기에 그만큼 치명적인 이슈로 다가왔다.
이전까지는 게임 초기 상태를 설정하는 initGame()이나 필요시에만 통신을 요청하는 등의 상황에서만 요청이 이루어졌기에 다행히도 데이터 교차 오류가 발생하진 않았지만, 프로젝트가 점차 완성되어감에 따라 통신 과정이 다양해졌고 프로젝트 막바지에 위와 같은 버그가 발생한 것이었다.
그림으로 표현하자면 다음과 같은 상황이라 말할 수 있겠다.
두가지 방법을 활용하여 문제를 해결하고자 했다.
먼저 사용했던 방법으로는 현재 원인이 소켓이 서버에 하나, 클라이언트에 하나로 1:1의 통신이 이루어지고 있기 때문에 일어난 일이라고 판단해 클라이언트의 요청에 구분을 두어 데이터 키워드를 달리하는 것이었다.
이후에 추가한 방법으로는 사실 위 방법보다 정석적인 방법이라고 생각이 드는데, 데이터 요청별로 핸들러의 아이디값을 추가하여 각자의 요청에 맞는 결과만을 resolve하도록 한 것이다.
자세한 내용은 다음과 같다.
이전에는 모두 'response' 라는 키워드로 데이터를 주고받았지만, 필요에 따라 다른 키워드를 사용하여 데이터를 주고받을 때 정확한 위치로 통신이 이루어질 수 있도록 하였다.
특히나 spawnMonster의 경우 내부 로직이 서버에서 일정 주기마다 신호만을 전달하는 형태인데, 이 주기가 다른 데이터 요청과 겹치게 될 경우 기대와 다른 결과가 반영될 수 있어 다른 키워드를 사용하는 것이 반필수적이었다.
서버에서 데이터를 전달할 때는 핸들러에서 처리한 후 단순한 return 을 통해 소켓에게 전달, 그 소켓이 socket.emit을 통해 클라이언트에게 반환하는 형태이다.
클라이언트는 이때 응답받은 data에 handlerId 값을 추가하여 요청에 맞는 핸들러 아이디를 가지고 있는 결과만, 그리고 그 결과값이 success 일 경우에만 data resolve를 실행하여 값을 반환하도록 처리하였다.
이러한 두가지 방법의 사용으로 본인의 요청에 맞는 결과만을 응답받아 사용할 수 있는 통신 구조가 완성되었다.
클라이언트에 보여지는 기능으로 추가하고자 했던 내용 중에 'Base의 체력 비율에 따라 이미지 변경' 기능이 있었다.
내부 로직으로는
0. 서버에서 Base의 최초값 저장 및 전달
1. 클라이언트에서 플레이 중 감소된 현재 Base의 체력 전달.
2. 서버에서 Base의 체력 비율 계산 및 이미지 변경 여부 판단
3. 이미지 변경 여부를 클라이언트로 전달
4. 클라이언트는 bool 값을 받아 Base의 이미지 변경
의 순서로 이루어지는데, 이 또한 기대와는 다른 상황이 연출되었다.
몬스터가 이동하여 Base에 도착 시 데미지를 입히고 몬스터는 사망하는 로직으로 이루어져 다음과 같은 코드 구성이 이루어진다.
문제가 되었던 것은 몬스터가 Base에 도착하고 데미지를 입혀 게임오버가 되는 순간이었다.
sendEvent(3, ~ ) 의 응답과 sendEvent(21, ~) 의 응답이 서로 다른 속도로 반환되고, 정확한 위치에 반환되지 못하자 response의 값을 읽지 못했고 그 결과 게임 오버가 실행되지 않거나, 이미지가 변경되지 않거나 혹은 둘 다 되지 않는 다양한 버그가 연출되었다.
위에서 언급한 두 가지 해결방안을 모두 테스트해보았지만 프레임마다 호출되는 gameLoop() 내부 로직에 구성된 코드였고, 그 안에서 또 for문을 통해 매우 빠른 속도로 반복 시행되는 로직이었기에 정확한 원인을 찾지 못했다.
결국 Base의 이미지 변경은 클라이언트 내부에서 자체적으로 판단, 처리하도록 구성하여 기능 자체는 완성하였지만 '웹소켓 통신' 이라는 프로젝트 주제에 맞지 않은 로직으로 수행된 것이 아쉬움으로 남는다.
웹소켓 통신 프로젝트는 이번이 두번째지만, 지난 RealTime 과는 다르게 이번에는 팀 단위로 프로젝트가 이루어졌다.
팀원들 간에 적절한 파트 분배와 정교한 서버 로직을 목표로 하였고, 개인적으로는 팀장으로서 팀원들 간의 소통을 잘 이끌고 지난 RealTime 에서 제대로 코드를 이해하지 못해 일어났던 시행착오를 일으키지 않는 것을 목표로 하였다.
팀원들 모두 웹소켓 통신 프로젝트를 팀 단위로 하는게 처음이라 적절한 파트 분배 등에서 여러 시행착오가 있었지만 결과적으로 프로젝트를 완성 시킬 수 있었고, 개인과 팀의 성장으로서 좋은 계기가 된 순간이었다고 회고할 수 있을 것 같다.
#5. 타워 디펜스 - KPT 회고 (1) | 2025.01.02 |
---|---|
#4. RealTime - 웹소켓 통신 게임 개발 (1) (3) | 2024.12.20 |
#3. 풋살 온라인 프로젝트 - KPT 회고 (0) | 2024.12.09 |
#3. 풋살 온라인 프로젝트 - Node.Js (4) | 2024.12.06 |
#2. 아이템 시뮬레이터 개발 - Node.Js(2) (1) | 2024.12.02 |