안녕하세요! 저는 Pixonic(MY.GAMES)의 서버 개발자인 Andrey Makhorin입니다. 이 기사에서는 우리 팀과 내가 어떻게 백엔드 개발을 위한 범용 솔루션을 만들었는지 공유하겠습니다. 개념과 결과, 그리고 Singularity라고 불리는 우리 시스템이 실제 프로젝트에서 어떻게 작동하는지 배우게 됩니다. 또한 우리가 직면한 어려움에 대해서도 자세히 살펴보겠습니다.
게임 스튜디오를 시작할 때 설득력 있는 아이디어를 신속하게 공식화하고 구현하는 것이 중요합니다. 수십 가지 가설이 테스트되고 게임은 지속적인 변화를 겪습니다. 새로운 기능이 추가되고 실패한 솔루션은 수정되거나 폐기됩니다. 그러나 이러한 빠른 반복 프로세스는 촉박한 기한 및 짧은 계획 기간과 결합되어 기술 부채가 축적될 수 있습니다.
기존 기술 부채가 있는 경우 기존 솔루션을 재사용하는 것은 다양한 문제를 해결해야 하기 때문에 복잡할 수 있습니다. 이것은 분명히 최적이 아닙니다. 하지만 또 다른 방법이 있습니다. 바로 "보편적 프레임워크"입니다. 일반적이고 재사용 가능한 구성 요소(예: 레이아웃 요소, 창, 네트워크 상호 작용을 구현하는 라이브러리)를 설계함으로써 스튜디오는 새로운 기능을 개발하는 데 필요한 시간과 노력을 크게 줄일 수 있습니다. 이 접근 방식은 개발자가 작성해야 하는 코드의 양을 줄일 뿐만 아니라 코드가 이미 테스트되었는지 확인하여 유지 관리에 소요되는 시간을 줄여줍니다.
우리는 한 게임의 맥락에서 기능 개발에 대해 논의했지만 이제 다른 각도에서 상황을 살펴보겠습니다. 모든 게임 스튜디오의 경우 프로젝트 내에서 작은 코드 조각을 재사용하는 것은 제작 간소화를 위한 효과적인 전략이 될 수 있지만 결국에는 새로운 히트 게임을 만들어야 합니다. 이론적으로 기존 프로젝트의 솔루션을 재사용하면 이 프로세스가 가속화될 수 있지만 두 가지 중요한 장애물이 발생합니다. 첫째, 동일한 기술적 부채 문제가 여기에 적용되고, 둘째, 기존 솔루션이 이전 게임의 특정 요구 사항에 맞게 조정되어 새 프로젝트에 적합하지 않을 가능성이 높습니다.
이러한 문제는 추가 문제로 인해 더욱 복잡해집니다. 데이터베이스 설계가 새 프로젝트의 요구 사항을 충족하지 못할 수 있고, 기술이 오래되었을 수 있으며, 새 팀에 필요한 전문 지식이 부족할 수 있습니다.
게다가 핵심 시스템은 특정 장르나 게임을 염두에 두고 설계되는 경우가 많아 새로운 프로젝트에 적응하기 어렵습니다.
다시 말하지만, 여기에서 범용 프레임워크가 작동하게 됩니다. 서로 크게 다른 게임을 만드는 것은 극복할 수 없는 도전처럼 보일 수 있지만 PlayFab, Photon Engine 및 유사한 플랫폼과 같이 이 문제를 성공적으로 해결한 플랫폼의 예가 있습니다. 개발 시간을 대폭 단축하여 개발자가 인프라보다는 게임 구축에 집중할 수 있는 능력을 입증했습니다.
이제 우리의 이야기로 뛰어 들어 봅시다.
멀티플레이어 게임의 경우 강력한 백엔드가 필수적입니다. 적절한 사례: 당사의 주력 게임인 War Robots입니다. 모바일 PvP 슈팅 게임으로, 출시된 지 10년이 넘었으며 백엔드 지원이 필요한 수많은 기능이 축적되어 있습니다. 그리고 우리 서버 코드는 프로젝트의 세부 사항에 맞게 조정되었지만 구식 기술을 사용하고 있었습니다.
새로운 게임을 개발할 때가 되었을 때, 우리는 War Robots의 서버 구성 요소를 재사용하는 것이 문제가 될 수 있다는 것을 깨달았습니다. 코드는 너무 프로젝트별로 특화되어 있었고 새 팀에는 부족한 기술에 대한 전문 지식이 필요했습니다.
우리는 또한 새 프로젝트의 성공이 보장되지 않으며 성공하더라도 결국 또 다른 새 게임을 만들어야 하며 동일한 "백지" 문제에 직면하게 될 것임을 인식했습니다. 이를 방지하고 미래에 대비하기 위해 우리는 게임 개발에 필요한 필수 구성 요소를 식별한 다음 모든 향후 프로젝트에서 사용할 수 있는 범용 프레임워크를 만들기로 결정했습니다.
우리의 목표는 개발자에게 백엔드 아키텍처 , 데이터베이스 스키마, 상호 작용 프로토콜 및 특정 기술을 반복적으로 설계할 필요가 없는 도구를 제공하는 것이었습니다. 우리는 사람들이 인증, 결제 처리, 사용자 정보 저장을 구현하는 부담에서 벗어나 게임 플레이, 디자인, 비즈니스 로직 등 게임의 핵심 측면에 집중할 수 있기를 원했습니다.
또한 우리는 새로운 프레임워크를 통해 개발을 가속화할 뿐만 아니라 클라이언트 프로그래머가 네트워킹, DBMS 또는 인프라에 대한 깊은 지식 없이도 서버 측 로직을 작성할 수 있도록 하고 싶었습니다.
그 외에도 DevOps 팀은 일련의 서비스를 표준화함으로써 IP 주소만 변경하여 모든 게임 프로젝트를 유사하게 처리할 수 있습니다. 이를 통해 재사용 가능한 배포 스크립트 템플릿과 모니터링 대시보드를 만들 수 있습니다.
프로세스 전반에 걸쳐 우리는 향후 게임에서 백엔드를 재사용할 가능성을 고려한 아키텍처 결정을 내렸습니다. 이러한 접근 방식을 통해 우리의 프레임워크는 유연하고 확장 가능하며 다양한 프로젝트 요구 사항에 적응할 수 있습니다.
(프레임워크 개발이 고립된 것이 아니라 다른 프로젝트와 병행하여 개발되었다는 점도 주목할 가치가 있습니다.)
우리는 Singularity에 다음을 포함하여 게임의 장르, 설정 또는 핵심 게임플레이에 구애받지 않는 일련의 기능을 제공하기로 결정했습니다.
이러한 기능은 모든 다중 사용자 모바일 프로젝트의 기본입니다(적어도 Pixonic에서 개발된 프로젝트와 관련이 있습니다).
이러한 핵심 기능 외에도 Singularity는 비즈니스 로직에 더 가까운 프로젝트별 기능을 더 많이 수용하도록 설계되었습니다. 이러한 기능은 추상화를 사용하여 구축되므로 다양한 프로젝트에서 재사용 및 확장이 가능합니다.
몇 가지 예는 다음과 같습니다.
기술적으로 Singularity 플랫폼은 다음 네 가지 구성 요소로 구성됩니다.
다음으로 각 구성 요소를 살펴보겠습니다.
프로필 서비스 및 매치메이킹과 같은 일부 서비스에는 게임별 비즈니스 로직이 필요합니다. 이를 수용하기 위해 우리는 이러한 서비스가 라이브러리로 배포되도록 설계했습니다. 그런 다음 이러한 라이브러리를 기반으로 구축함으로써 개발자는 명령 처리기, 매치메이킹 논리 및 기타 프로젝트별 구성 요소를 통합하는 애플리케이션을 만들 수 있습니다.
이 접근 방식은 프레임워크가 낮은 수준의 HTTP 프로토콜 기능을 제공하는 반면 개발자는 비즈니스 논리가 포함된 컨트롤러와 모델을 만드는 데 집중할 수 있는 ASP.NET 응용 프로그램을 구축하는 것과 유사합니다.
예를 들어, 게임 내에서 사용자 이름을 변경하는 기능을 추가하고 싶다고 가정해 보겠습니다. 이를 위해 프로그래머는 새 사용자 이름과 이 명령에 대한 처리기를 포함하는 명령 클래스를 작성해야 합니다.
다음은 ChangeNameCommand의 예입니다.
public class ChangeNameCommand : ICommand { public string Name { get; set; } }
이 명령 처리기의 예:
class ChangeNameCommandHandler : ICommandHandler<ChangeNameCommand> { private IProfile Profile { get; } public ChangeNameCommandHandler(IProfile profile) { Profile = profile; } public void Handle(ICommandContext context, ChangeNameCommand command) { Profile.Name = command.Name; } }
이 예에서 처리기는 종속성 주입을 통해 자동으로 처리되는 IProfile 구현으로 초기화되어야 합니다. IProfile, IWallet, IInventory 등 일부 모델은 추가 단계 없이 구현이 가능합니다. 그러나 이러한 모델은 데이터를 제공하고 특정 프로젝트 요구 사항에 맞지 않는 인수를 수용하는 추상적인 특성으로 인해 작업하기가 그리 편리하지 않을 수 있습니다.
작업을 더 쉽게 하기 위해 프로젝트는 자체 도메인 모델을 정의하고 이를 핸들러와 유사하게 등록하고 필요에 따라 생성자에 주입할 수 있습니다. 이 접근 방식을 사용하면 데이터 작업 시 더욱 맞춤화되고 편리한 환경을 얻을 수 있습니다.
다음은 도메인 모델의 예입니다.
public class WRProfile { public readonly IProfile Raw; public WRProfile(IProfile profile) { Raw = profile; } public int Level { get => Raw.Attributes["level"].AsInt(); set => Raw.Attributes["level"] = value; } }
기본적으로 플레이어 프로필에는 Level 속성이 포함되어 있지 않습니다. 그러나 프로젝트별 모델을 생성하면 이러한 속성을 추가할 수 있으며 명령 처리기에서 플레이어 수준 정보를 쉽게 읽거나 변경할 수 있습니다.
도메인 모델을 사용하는 명령 처리기의 예:
class LevelUpCommandHandler : ICommandHandler<LevelUpCommand> { private WRProfile Profile { get; } public LevelUpCommandHandler(WRProfile profile) { Profile = profile; } public void Handle(ICommandContext context, LevelUpCommand command) { Profile.Level += 1; } }
해당 코드는 특정 게임의 비즈니스 논리가 기본 전송 또는 데이터 저장 계층과 분리되어 있음을 명확하게 보여줍니다. 이러한 추상화를 통해 프로그래머는 트랜잭션성, 경쟁 조건 또는 기타 일반적인 백엔드 문제에 대해 걱정하지 않고 핵심 게임 메커니즘에 집중할 수 있습니다.
더욱이 Singularity는 게임 로직을 향상시키기 위한 광범위한 유연성을 제공합니다. 플레이어의 프로필은 "키 유형 값" 쌍의 모음이므로 게임 디자이너가 원하는 대로 모든 속성을 쉽게 추가할 수 있습니다.
프로필 외에도 Singularity의 플레이어 엔터티는 유연성을 유지하도록 설계된 몇 가지 필수 구성 요소로 구성됩니다. 특히 여기에는 플레이어의 아이템을 나열하는 인벤토리뿐만 아니라 각 통화의 금액을 추적하는 지갑도 포함됩니다.
흥미롭게도 Singularity의 항목은 프로필과 유사한 추상 개체입니다. 각 항목에는 고유 식별자와 키 유형 값 쌍 세트가 있습니다. 따라서 아이템이 반드시 게임 세계의 무기, 의복, 자원과 같은 유형의 개체일 필요는 없습니다. 대신 퀘스트나 제안과 같이 플레이어에게 고유하게 발행된 일반적인 설명을 나타낼 수 있습니다. 다음 섹션에서는 이러한 개념이 특정 게임 프로젝트 내에서 어떻게 구현되는지 자세히 설명하겠습니다.
Singularity의 주요 차이점 중 하나는 항목이 대차대조표의 일반적인 설명에 대한 참조를 저장한다는 것입니다. 이 설명은 그대로 유지되지만 발행된 개별 품목의 속성은 변경될 수 있습니다. 예를 들어, 플레이어는 무기 스킨을 변경할 수 있습니다.
또한 플레이어 데이터를 마이그레이션하기 위한 강력한 옵션도 있습니다. 기존 백엔드 개발에서는 데이터베이스 스키마가 비즈니스 논리와 밀접하게 결합되는 경우가 많으며 엔터티 속성을 변경하려면 일반적으로 직접적인 스키마 수정이 필요합니다.
그러나 전통적인 접근 방식은 프레임워크가 플레이어 엔터티와 관련된 비즈니스 속성에 대한 인식이 부족하고 게임 개발 팀이 데이터베이스에 직접 액세스할 수 없기 때문에 Singularity에 적합하지 않습니다. 대신 마이그레이션은 직접적인 저장소 상호 작용 없이 작동하는 명령 처리기로 설계 및 등록됩니다. 플레이어가 서버에 연결하면 데이터베이스에서 데이터를 가져옵니다. 서버에 등록된 마이그레이션이 아직 이 플레이어에 적용되지 않은 경우 마이그레이션이 실행되고 업데이트된 상태가 데이터베이스에 다시 저장됩니다.
적용된 마이그레이션 목록도 플레이어 속성으로 저장되며 이 접근 방식에는 또 다른 중요한 이점이 있습니다. 즉, 시간이 지남에 따라 마이그레이션을 시차를 두고 수행할 수 있다는 것입니다. 이를 통해 모든 플레이어 기록에 새 속성을 추가하고 이를 기본값으로 설정하는 등 대규모 데이터 변경으로 인해 발생할 수 있는 가동 중지 시간 및 성능 문제를 방지할 수 있습니다.
Singularity는 백엔드 상호 작용을 위한 간단한 인터페이스를 제공하므로 프로젝트 팀은 프로토콜이나 네트워크 통신 기술 문제에 대해 걱정하지 않고 게임 개발에 집중할 수 있습니다. (즉, SDK는 필요한 경우 프로젝트별 명령에 대한 기본 직렬화 방법을 재정의할 수 있는 유연성을 제공합니다.)
SDK를 사용하면 API와의 직접적인 상호 작용이 가능하지만 일상적인 작업을 자동화하는 래퍼도 포함되어 있습니다. 예를 들어 프로필 서비스에서 명령을 실행하면 플레이어 프로필의 변경 사항을 나타내는 일련의 이벤트가 생성됩니다. 래퍼는 이러한 이벤트를 로컬 상태에 적용하여 클라이언트가 프로필의 현재 버전을 유지하도록 합니다.
다음은 명령 호출의 예입니다.
var result = _sandbox.ExecSync(new LevelUpCommand())
Singularity 내의 대부분의 서비스는 다목적으로 설계되었으며 특정 프로젝트에 대한 사용자 정의가 필요하지 않습니다. 이러한 서비스는 사전 구축된 애플리케이션으로 배포되며 다양한 게임에서 활용될 수 있습니다.
기성 서비스 제품군에는 다음이 포함됩니다.
인증 서비스 및 게이트웨이와 같은 일부 서비스는 플랫폼의 기본이며 배포해야 합니다. 그 외 친구 서비스, 리더보드 등은 선택 사항으로, 이를 필요로 하지 않는 게임 환경에서는 제외될 수 있습니다.
다수의 서비스 관리와 관련된 문제는 나중에 다루겠지만 지금은 선택 서비스가 선택 사항으로 남아 있어야 한다는 점을 강조하는 것이 중요합니다. 서비스 수가 증가함에 따라 새로운 프로젝트의 복잡성과 온보딩 임계값도 증가합니다.
Singularity의 핵심 프레임워크는 매우 유능하지만 핵심 기능을 수정하지 않고도 프로젝트 팀이 독립적으로 중요한 기능을 구현할 수 있습니다. 기능이 여러 프로젝트에 잠재적으로 유용한 것으로 식별되면 프레임워크 팀에서 이를 개발하고 별도의 확장 라이브러리로 출시할 수 있습니다. 그런 다음 이러한 라이브러리를 게임 내 명령 처리기로 통합하고 활용할 수 있습니다.
여기에 적용될 수 있는 몇 가지 예시 기능은 퀘스트와 제안입니다. 핵심 프레임워크의 관점에서 보면 이러한 엔터티는 단순히 플레이어에게 할당된 항목일 뿐입니다. 그러나 확장 라이브러리는 이러한 항목에 특정 속성과 동작을 부여하여 퀘스트나 제안으로 변환할 수 있습니다. 이 기능을 사용하면 아이템 속성을 동적으로 수정할 수 있어 퀘스트 진행 상황을 추적하거나 플레이어에게 제안이 제공된 마지막 날짜를 기록할 수 있습니다.
Singularity는 전 세계적으로 사용 가능한 최신 게임 중 하나인 Little Big Robots에 성공적으로 구현되었으며, 이를 통해 클라이언트 개발자는 서버 로직을 직접 처리할 수 있는 능력을 갖게 되었습니다. 또한 플랫폼 팀의 직접적인 지원 없이도 기존 기능을 활용하는 프로토타입을 만들 수 있었습니다.
그러나 이 보편적인 솔루션에는 어려움이 따르지 않습니다. 기능 수가 늘어남에 따라 플랫폼과 상호 작용하는 것도 복잡해졌습니다. Singularity는 단순한 도구에서 정교하고 복잡한 시스템으로 진화했습니다. 이는 기본 푸시 버튼 전화기에서 모든 기능을 갖춘 스마트폰으로 전환하는 것과 어떤 면에서는 유사합니다.
Singularity는 개발자가 데이터베이스 및 네트워크 통신의 복잡성을 탐구할 필요성을 완화하는 동시에 자체 학습 곡선도 도입했습니다. 이제 개발자는 Singularity 자체의 미묘한 차이를 이해해야 하며 이는 상당한 변화가 될 수 있습니다.
개발자부터 인프라 관리자까지 다양한 사람들이 이러한 과제에 직면해 있습니다. 이러한 전문가는 Postgres 및 Kafka와 같은 잘 알려진 솔루션을 배포하고 유지 관리하는 데 깊은 전문 지식을 보유하고 있는 경우가 많습니다. 그러나 Singularity는 내부 제품이므로 새로운 기술을 습득해야 합니다. Singularity 클러스터의 복잡성을 배우고, 필수 서비스와 선택 서비스를 구별하고, 모니터링에 중요한 지표가 무엇인지 이해해야 합니다.
회사 내에서 개발자가 언제든지 플랫폼 제작자에게 연락하여 조언을 구할 수 있는 것은 사실이지만 이 프로세스에는 필연적으로 시간이 필요합니다. 우리의 목표는 진입 장벽을 최대한 최소화하는 것입니다. 이를 달성하려면 각각의 새로운 기능에 대한 포괄적인 문서가 필요하므로 개발 속도가 느려질 수 있지만 그럼에도 불구하고 플랫폼의 장기적인 성공을 위한 투자로 간주됩니다. 또한 시스템 신뢰성을 보장하려면 강력한 단위 및 통합 테스트 범위가 필수적입니다.
Singularity는 수동 테스트를 위해서는 별도의 게임 인스턴스를 개발해야 하는데 이는 비실용적이기 때문에 자동화된 테스트에 크게 의존합니다. 자동화된 테스트는 대다수, 즉 99%의 오류를 포착할 수 있습니다. 그러나 항상 특정 게임 테스트 중에만 명백히 드러나는 소수의 문제가 있습니다. Singularity 팀과 프로젝트 팀은 비동기적으로 작업하는 경우가 많기 때문에 이는 릴리스 일정에 영향을 미칠 수 있습니다. 오래 전에 작성된 코드에서 차단 오류가 발견될 수 있으며, 플랫폼 개발 팀은 또 다른 중요한 작업에 전념할 수 있습니다. (이 문제는 Singularity에만 국한된 것이 아니며 사용자 지정 백엔드 개발에서도 발생할 수 있습니다.)
또 다른 중요한 과제는 Singularity를 사용하는 모든 프로젝트의 업데이트를 관리하는 것입니다. 일반적으로 지속적인 기능 요청 및 개선을 통해 프레임워크 개발을 주도하는 하나의 주력 프로젝트가 있습니다. 이 프로젝트 팀과의 상호 작용은 긴밀하게 이루어집니다. 우리는 그들의 요구 사항과 그들이 우리 플랫폼을 활용하여 문제를 해결할 수 있는 방법을 이해합니다.
일부 주요 프로젝트는 프레임워크 팀과 밀접하게 관련되어 있지만 초기 개발 단계의 다른 게임은 기존 기능과 문서에만 의존하여 독립적으로 운영되는 경우가 많습니다. 개발자가 문서를 오해하거나 사용 가능한 기능을 오용할 수 있으므로 이로 인해 중복되거나 최적이 아닌 솔루션이 발생할 수 있습니다. 이를 완화하려면 프레젠테이션, 모임, 팀 교류를 통해 지식 공유를 촉진하는 것이 중요합니다. 하지만 이러한 계획에는 상당한 시간 투자가 필요합니다.
Singularity는 이미 게임 전반에 걸쳐 그 가치를 입증했으며 더욱 발전할 준비가 되어 있습니다. 새로운 기능을 도입할 계획이지만 현재 우리의 주요 초점은 이러한 개선 사항으로 인해 프로젝트 팀의 플랫폼 유용성이 복잡해지지 않도록 하는 것입니다.
이 외에도 진입 장벽을 낮추고 배포를 단순화하며 분석 측면에서 유연성을 추가하여 프로젝트가 솔루션을 연결할 수 있도록 해야 합니다. 이는 팀에게는 어려운 일이지만, 실제로 우리 솔루션에 투자한 노력은 확실히 전액 보상을 받을 것이라고 믿고 있습니다!