개요
게임 소켓 서버 개발을 하면서 다양한 문제점들이 발생하였다, 대부분 사소하게 무언가 빼먹던가 메모리 관리가 제대로 되지 않아 누수가 있어 발생한 문제들로 API호출 요청에 실패하거나 서버가 다운되는 크리티컬한 이슈 등 라이브 환경에서 일어나면 안될 버그가 있었다.
이슈 발생이 의심되는 부분의 코드 검토와 디버깅 로그를 삽입하여 동작 테스트를 하며 대부분의 이슈는 해결이 되었지만, 클라이언트와 통신을 통해 발생하는 경우 해당 이슈가 클라이언트에서 발생하는지, 서버에서 발생하는지에 대해 알기 어려웠다.
다행히 클라이언트의 네트워크 부분을 담당하는 팀원과 자주 소통을하여 문제점을 하나하나 잡아가기 시작했고, 이러한 소통을 통해 트러블 슈팅을 더 빠르게 해결한 경험이 되었다.
하기에 내가 프로젝트를 진행하며 겪었던 이슈들과 해결을 위해 어떤 조치를 취했는지를 기록하고자 한다.
패키지 종속성 문제
초기에 라이브러리를 설치하고 적용하기 위해 라이브러리를 설치 후 시스템 변수를 통해 전역에서 사용 가능하도록 할지, 프로젝트 별 독립적으로 라이브러리를 설치해 줄지를 고민하였다. 결론은 어떤 환경에서도 레포지토리를 clone받은 후 실행할 수 있는 환경을 만들기 위해 프로젝트별 독립적으로 라이브러리를 설치하는 방향으로 결정하였다.
따라서 vcpkg를 선택하게 되었으며, 설치해야할 라이브러리 파일을 vcpkg.json으로 목록화하여 필요한 패키지를 자동으로 설치하게 하는 파우쉘(윈도우), 쉘 스크립트(리눅스)를 제작하여 라이브러리 설치를 자동화 하였다. 하지만 해당 작업은 시간이 많이 소요되는 편이어서 다음에 동일한 작업을 하게 된다면 시간을 더 줄일 수 있는 방법에 대해 생각해 보려고 한다.
크로스플랫폼 빌드 문제
CMake를 사용하면 윈도우 및 리눅스 환경에서 빌드 시 크로스플랫폼 문제를 쉽게 해결할 수 있다는 글을 접하였다. 따라서 테스트는 윈도우에서, 배포는 리눅스에서 진행할 예정이었기에 CMake를 통해 컴파일 및 빌드에 필요한 소스 파일 및 라이브러리를 명시해 주었으나, 리눅스에서 동일한 CMakeLists.txt파일로 빌드 시 오류가 발생하였다.
오류 발생 이유는 vcpkg로 설치하는 라이브러리의 OS별 제공되는 버전이나 깃 레포지토리 명이 달랐기 때문이었다, 해당 부분을 수정하려면 결국 플랫폼별 CMake파일을 별도로 관리해 주어야 하기 때문에 리눅스 환경에선 Makefile을 통한 g++빌드를 하는 것으로 변경하였다.
Spring과 같은 프레임워크를 사용한 프로젝트와 달리 C++프로젝트는 컴파일에 시간이 많이 소요되는 편이었다, 이를 개선하기 위해 헤더파일간 종속성을 최소화 하고, ccache를 통해 빌드 캐시를 구성, 기타 라이브러리 체크 등도 캐싱을 사용하여 이전 빌드의 스냅샷을 통해 변경된 부분이 없을 경우 빠르게 빌드가 될 수 있도록 구현하였다, 이로 인해 평균 1분 30초 정도 걸리던 빌드가 대폭 감소하게 되었다. 감소 수치는 얼마나 소스 코드를 변경하고 재빌드하냐에 따라 달랐다.
DB 트랜잭션 관련 문제
레포지토리 계층에서 DB와 통신 중 모종의 이유로 인해 트랜잭션 도중 실패를 반환할 경우 DB풀에 커넥션을 반환하는 작업을 해주지 않았었다. 커넥션이 uniq_prt스마트포인터로 관리되고 있기 때문에 코드 블럭을 벗어날 경우 자동으로 반환될 줄 알았으나 반환해 주지 않을 경우 해당 커넥션이 사용 가능한지 여부를 체크할 수 없었다.
이로 인해 서버 시작 시 생성했던 DB풀의 크기보다 많은 트랜잭션 예외가 발생했을 경우 더 이상 사용할 수 있는 DB커넥션이 없기 때문에 커넥션을 새로 생성하고 DB풀에 삽입해 주는 과정이 발생하였다, 이로 인해 심각한 메모리 누수가 발생하였다.
다행히 서버 개발 초기에 이를 발견하여 트랜잭션 도중 예외가 발생하여 abort처리가 필요할 경우 사용했던 DB커넥션을 DB풀로 반환해 주는 로직을 작성하여 이를 해결하였다.
spdlog의 format관련 문제
해당 프로젝트에서는 Json타입의 패킷을 사용하여 클라이언트와 통신하였다. 따라서 요청이 알맞게 도착했는지를 검증하기 위해 request에 필요한 요소가 모두 존재하는지, 요소의 변수 타입이 일치하는지 등을 서비스 계층에서 체크해주었다. 하지만 spdlog에서는 원시적으로 전달된 Json객체의 key값만으로 value를 바로 참조하고자 하면 서버가 다운되는 크리티컬한 이슈가 발생하였다.
이를 통해 타입 추론이 불가능한 경우 명시적으로 타입을 지정해 주어야 런타임 중 예외가 발생하지않도록 방지할 수 있다는 것을 깨달았다, .get<int>(), .as<std::string>()등을 통해 타입을 직접 명시해주어 해당 문제를 해결하였다.
동기화 처리 문제
우선 Boost.asio를 통해 소켓 서버 개발을 진행하면서 io_context를 통한 병렬 작업에 대한 많은 경험치를 쌓았다. 이러한 멀티스레드 환경에서 서버 내의 공유 자원을 안전하게 관리하고 참조할 수 있도록 하는 방법에 대해 많은 고민을 해보았다.
우선 서버에 세션이 연결되어 관련 자료구조에 신규로 연결된 삽입, 삭제, 수정등의 작업이 수행되어야 할 경우 해당 자료구조에 mutex를 활용한 lock을 걸어주어야 한다는 것은 CS스터디를 통해 얻은 이론을 통해 알고 있었다. 하지만 실제 프로젝트를 통해 이를 구현하는 것은 처음이었어서 매우 만족스러운 시간이었다.
또 말로만 듣던 데드락이 발생할 수 있는 환경에 대한 대처를 하였다, 공유 자원을 서로 점유 대기하는 상태를 만들지 않기 위해 노력하였으며, 프로세스를 진행할 때 낙관적 락을 통해 작업 병렬처리를 보장해주었다. 그 결과 이번 프로젝트에선 데드락이 발생하지 않았고, 추후엔 직접 테스트 환경에서 데드락이 발생하고 이를 해결하는 경험을 쌓고 싶다.
보안 문제
실제 배포 환경에서 사용자가 로그인을 하였을 때 설정한 비밀번호와 같은 데이터를 DB에 저장할 때 실제 서비스 제공자도 해당 정보를 몰라야 한다는 것은 이론적으로 알고 있었다. 이번 프로젝트 배포를 통해 약 170명의 실제 서비스 이용자가 생겼고, 해당 사용자들의 민감 데이터를 해싱하여 저장하는 방법을 적용하였다.
비록 강력한 암호화를 제공하지는 않는 SHA-256알고리즘이지만, 이를 통해 사용자의 민감 데이터를 암호화하여 DB에 저장하는 경험을 하게 되어서 뜻깊었다.
세션 관리 문제
stateful한 서버에서 연결된 세션에 대한 관리에 대한 트러블슈팅이 유난히 많았다. 클라이언트 세션이 연결되어 서버측 자료구조에 해당 세션에 대한 포인터를 저장하고, 이 과정에서 뮤텍스로 락을 걸어 다른 스레드가 해당 자료구조에 접근할 수 없도록 구현하는 점이 첫 번째 도전 과제였다. 위에서 언급한 것과 마찬가지로 이론으로만 알던 사실을 실제로 구현해 보아 뜻깊었다.
이번 소켓 서버 개발을 통해 도전했던 세션 관련한 과제는 다음과 같다.
- 세션 체크아웃을 통해 연결이 끊긴 세션을 12초 간격으로 탐색하고 서버에서 제거
- 일반 사용자와 데디케이트 서버의 호스트 세션을 분리하여 관리
- 서버→세션 브로드캐스팅을 활용한 푸시 기능 구현(방 생성 시 클라이언트가 입력한 방 정보 데디케이트 서버로 전달, 실시간 동접자 정보를 로비에 존재하는 클라이언트에게 3초 간격으로 브로드캐스팅)
- 이미 접속중인 사용자인 경우 소켓 연결 반려
- 이미 접속중인 IP인 경우 소켓 연결 반려
IP 관리 문제
세션의 소멸자에서 현재 세션의 IP주소를 서버의 자료구조에서 삭제하는 로직을 구현했었다. IP주소값은 소켓의 멤버함수인 remote_endpoint()를 호출하고, .address().to_string()를 통해 IP주소를 문자열로 파싱하였다. 문제는 소멸자가 호출되는 시점에 해당 함수를 호출했을때 소켓의 IP값이 변경이 되었는지 참조를 하는 순간 서버가 다운되는 크리티컬한 이슈가 발생하였다.
이를 해결하기 위해 세션의 생성자에서 소켓의 IP주소를 받아 멤버 변수 remote_ip_에 할당해 주었고, 소멸자에서 remote_ip_를 활용하여 서버의 자료구조에서 해당 ip를 삭제해주는 로직으로 변경하여 서버가 다운되는 현상을 해결하였다.
회고
서버개발을 하며 정말 많은 이슈가 발생하였다. 윈도우 환경에서는 돌아갔던 서버가 리눅스 환경에서 빌드하니 에러를 반환하는 경우도 있었고, 정말 사소한 오타 하나 혹은 놓친 부분이 런 타임 환경에서 서버를 다운시키는 등 실제 서비스에서 벌어졌으면 큰일 났을 정도의 크리티컬한 이슈로 이어졌다.
이러한 현상을 고치고자 디버깅 코드를 다수 삽입하였고, 주석을 써내려가며 코드 리팩토링을 진행했다. 추가적으로 신규 기능을 개발할 때에 최대한 크리티컬 이슈 발생을 억제하고자 예외 처리와 방어 코드를 작성하였고, 실제 배포 시점 이후로는 서버가 다운되는 현상을 다행히도 일어나지 않았다.
'프로젝트 > 메타버스 게임' 카테고리의 다른 글
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 CI/CD 파이프라인 구축 (0) | 2025.04.10 |
---|---|
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 docker 컨테이너화 (1) | 2025.04.10 |
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 환경변수 세팅(.env) (0) | 2025.04.09 |
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 유틸리티 (1) | 2025.04.09 |
[메타버스 게임] 캐쥬얼 배틀로얄 프로젝트 레포지토리 계층 (1) | 2025.04.09 |