개발 기간 약 1년의 모바일 게임 신규 개발 프로젝트에서, 인원이 늘어도 개발 속도가 떨어지지 않도록 하기 위해 초기에 내린 의사결정과 도입한 구조·테스트·워크플로·툴링을 정리한다.
URL: https://blog.sge-coretech.com/entry/2025/12/16/115322
안녕하세요, 주식회사 사이버에이전트 SGE 코어기술본부(코어테크)의 엔지니어 야노입니다.
저는 지난 1년 정도, 한 모바일 게임의 신규 개발에 참여했습니다.
이 프로젝트는 개발 기간이 약 1년으로, 당사 내부 기준으로는 짧은 기간에 출시를 진행한 프로젝트였습니다.
짧은 기간의 개발에서는 “처음에는 빨랐는데, 사람이 늘어날수록 점점 개발이 느려진다”는 상황에 빠지는 것을 반드시 피해야 합니다.
그래서 저는 서브 미션(메인은 별도)으로 “개발 효율 최적화”를 내세우고, 프로젝트 초기부터 커밋해 왔습니다.
배움이 많은 프로젝트였기에, 특히 프로젝트 초기를 중심으로 어떤 의사결정을 했고 어떤 장치를 도입했는지, 그 일부를 소개하고자 합니다.
모든 것을 상세히 쓰면 분량이 너무 커지기 때문에, 이번에는 개요 수준의 정리입니다. 그래도 프로젝트를 시작하거나 개발 효율 개선에 임하는 분들께 참고가 되면 좋겠습니다.
본 글에서는 소프트웨어 아키텍처나 워크플로 같은 주제를 다루지만, 이들의 최적해는 제품 규모나 팀 구성, 예산, 개발 기간 등의 컨텍스트에 크게 의존합니다.
따라서 원리주의적으로 과도한 것 등 비건설적인 논쟁의 불씨를 만들지 않기 위해, 먼저 프로젝트의 전제를 적어둡니다. 물론 건설적인 논의는 환영합니다.
프로젝트 규모감
기능이 꽤 많은 하이브리드 캐주얼 게임
제품 규모 - 화면 수: 약 97 화면
엔지니어 인원: 많을 때 월 실동 8인월 정도
출시까지 개발 기간: 약 1년
기타
서버-클라이언트 방식
다국어 지원
하이브리드 캐주얼 장르치고 요구사항이 많은 편
주니어가 많은 편이고 베테랑도 있음
사양의 불확실성은 낮은 프로젝트
Unity로 개발
이후 소개하는 사례는 어디까지나 위 전제 조건을 가진 이 프로젝트에서, 또한 제 현재 역량이라는 제약 아래에서의 하나의 해법일 뿐입니다.
은탄환은 존재하지 않으니, 자신이 처한 제약과 문화 등에 맞춰 최적해를 생각하는 것이 우리 엔지니어의 일이라는 전제하에, 하나의 사례로 읽어주시면 좋겠습니다.
프로젝트의 초기(본 개발의 초기)는 엔지니어에게 매우 쾌적한 시기입니다.
읽어야 할 기존 코드도 없고, 빌드는 금방 끝나며, 써보고 싶었던 최신 라이브러리도 도입할 수 있습니다.
눈에 보이는 아웃풋을 내기 쉬울 뿐 아니라, 일반적으로 그런 명확한 아웃풋이 평가받기 쉬운 조직 역학이 작동하기 쉬운 시기이기도 합니다.
그렇기에 이 시기는 “설계보다 지금은 속도 우선으로”, “폴더 구성은 나중에 정하면 되지” 같은 ‘일단’ 판단이 횡행하기 쉬운 시기라고도 생각합니다.
이런 판단은 눈앞의 일만 보면 최적일 수 있습니다. 빠르게 아웃풋이 나오고 평가되기 쉬우며, 기술적 난이도도 낮습니다.
하지만 중장기적으로 갚아야 하는 기술적 부채를 만들어내고 있다고도 볼 수 있습니다.
그리고 부채에는 이자가 붙습니다. 기술적 부채에도 이자가 붙고, 그 이자는 프로젝트가 진행될수록 복리처럼 불어납니다.
예를 들어 지저분한 코드가 생기면 그 위에 임시방편 코드가 덧대어지고, 해독이 어려워지고, 버그가 생기며, 이를 고치기 위해 더 애드혹한 대응이 추가되는 루프가 발생한다는 뜻입니다.
게다가 이 부채를 갚을 때에는 처음 코드를 쓴 사람이 이미 프로젝트에 없을 수도 있습니다.
그 경우 다른 사람이 부채를 상환하게 됩니다. 이런 점은 현실의 부채와의 아날로지와 크게 다른 점일지도 모릅니다.
코드 예시 외에도, 예를 들면 문서가 없어 전문가에게 물어보지 않으면 모르는 상황이라든지, 반대로 관리해야 할 문서가 너무 많아 문서를 읽고 쓰는 데 시간을 과도하게 쓰는 상황도 생각할 수 있습니다.
프로젝트 후반에 개발 속도가 떨어지는 것은, 이렇게 불어난 이자 지급에 공수의 대부분을 빼앗긴 상태라고 할 수 있습니다.
본 프로젝트에서는 단기 개발을 하기 위해, 우선 이런 부채를 가능한 한 만들지 않는 것을 중요하게 여겼습니다.
부채를 만들지 않기 위해 ‘일단’으로 해서는 안 되는 것의 예로, Unity 프로젝트의 폴더 구조가 있습니다.
“목표 파일을 찾는” 작업은, 엔지니어/비엔지니어를 불문하고 Unity를 쓰는 모든 멤버가 매우 빈번하게 하는 작업입니다.
한 번당 시간은 몇 초~수십 초에 불과하더라도, 프로젝트 전체로 누적되면 막대한 공수가 됩니다.
예를 들어 샘플 등에서 흔한, 루트부터 파일 종류(Prefabs, Scenes 등)로 폴더를 나누는 구성은 다음과 같은 문제가 생길 수 있습니다.
Scenes 폴더를 열어야 하는지 Prefabs 폴더를 열어야 하는지 직관적으로 알기 어렵다Prefabs/Outgame/Shop과 Textures/Outgame/Shop처럼 떨어진 계층을 자주 오가야 한다이를 방지하기 위해, 본 프로젝트에서는 다음 점을 중시해 구성을 정했습니다.
구체적으로는 “지금 하려는 작업 단위”로, 루트에 가까운 계층부터 크게 폴더를 나누는 폴더 구조로 했습니다.
개인적으로 이를 컨텍스트 퍼스트 폴더 구조라고 부르고 있습니다.
Assets
└── (중략·프로젝트명 등)
├── 3d # 3D 모델
├── Graphic # 셰이더 등 그래픽 엔지니어가 쓰는 것
├── UI # UI 관련 프리팹·이미지·씬 등
├── MasterData # 마스터 데이터
├── Scripts # 스크립트는 어셈블리 관계가 있으므로 묶어서
└── ...
이렇게 하면 “UI를 편집하고 싶다”고 생각했을 때 망설임 없이 UI 폴더를 열고 작업을 시작할 수 있습니다.
실제로는 더 세밀하게 정했지만, 기본적으로는 작업자가 헤매지 않는 것을 최우선으로 한 폴더 구조입니다.
또한 이 폴더 구조 이야기는 어디까지나 한 예입니다.
중요한 것은 정보에 대한 접근 경로를 최적화하고, 인지 비용을 다른 사람에게 지불하게 하지 않는 것이 중장기 생산성으로 이어진다는 점입니다.
폴더 구조 외에, 다음도 생각해두면 좋습니다.
개발용 공통 언어(유비쿼터스 언어)
문서
약어
등등
사소해 보이지만 이렇게 정보를 정리해두면, 개발이 진행되어 폴더나 문서 등 정보가 늘어났을 때 “사람이 늘었는데도 느려진다”는 사태를 미연에 방지할 수 있습니다.
이 “사람이 늘었는데도 느려진다”는 현상은 브룩스의 법칙으로 알려져 있습니다.
브룩스의 법칙은 그 근거로, 앞 절의 예처럼 커뮤니케이션 비용 외에 작업의 분해 가능성을 들고 있습니다.
예를 들어 극단적인 예로, 게임의 모든 기능을 단 하나의 Scene에 배치하고, Prefab화도 하지 않은 채로 GameObject를 직접 늘어놓아 만들었다고 합시다.
이 상태에서는 Unity의 구조상 여러 사람이 동시에 그 Scene을 편집하기 어렵습니다.
누군가 작업하는 동안 다른 사람은 대기하거나, 나중에 충돌한 작업을 머지해야 합니다. 그리고 이 머지 작업은 대개 현실적이지 않습니다.
즉 이 프로젝트는 작업의 분해 가능성이 현저히 낮은 상태이며, 사람을 늘려도 병렬 작업이 불가능하고 오히려 대기 시간과 충돌 해결로 속도가 떨어집니다.
이건 극단적 예시지만, 실제 프로젝트에서도 “어떤 기능을 건드리면 다른 기능이 깨진다”, “로직과 표시가 밀결합이라 UI 디자이너가 스스로 조정 작업을 못 한다” 같은 형태로 병렬 작업의 방해 요인이 생기기 쉽습니다.
그래서 이른바 아웃게임 영역에 대해서는 몇 가지 설계적 공을 들였습니다.
우선 비즈니스 로직(외형과 무관한 로직)의 구현 비중이 큰 프로젝트였기 때문에, 프레젠테이션 로직(외형 관련 로직) 구현 담당과 비즈니스 로직 구현 담당을 나눌 수 있도록 설계했습니다.
그 위에서 비즈니스 로직을 게임 내 기능 단위로 분할해, 각각 독립적으로 다른 담당자가 구현하고 테스트할 수 있게 했습니다(물론 의존 관계가 있는 기능에는 로직에도 의존이 생깁니다).
또한 뷰는 비즈니스 로직 구현과 병행할 수 있도록, 도메인 모델을 참조하지 않는 형태로 설계했습니다.
더 나아가 후술하듯, 프리젠터에도 의존하지 않고 뷰 단독으로 동작 확인·UI 디자이너에 의한 조정 등이 가능하도록 하는 장치를 만들었습니다.

구성도
또한 이런 아키텍처 측면 외에도, 나중에 사람이 늘어나도 부드럽게 움직일 수 있도록 프로젝트 초기에 다음을 정했습니다.
모델 접근 방침
화면 전환 구현 방법
글로벌 접근 방침
학습 비용 최적화 방침
메시징(이벤트 통지) 방침
어셈블리 활용 방침
internal로 두어 외부 인터페이스를 명확히 한다등
세부나 바꿔 끼울 수 있는 부분은 나중에 누군가 정해도 되므로, 프로젝트 초기로서는 프레임워크·토대가 되는 부분을 중심으로 정해두었습니다.
또한 물론 이런 것들을 혼자 정한 것은 아니고, 다른 엔지니어 분들께 상담하며 정했습니다.
다행히도 상담 과정에서 몇 가지 방침이 더 좋은 방향으로 바뀌었습니다.
앞 절에서 언급했듯, 본 프로젝트에서는 비즈니스 로직 테스트를 작성했습니다.
본 프로젝트는 서버 사이드에도 많은 로직이 있지만, 클라이언트 사이드 비즈니스 로직만 해도 882건(집필 시점)의 테스트가 존재합니다.

Test Runner 실행 화면
이를 통해 출시 이후에도 로직 자체가 원인인 버그는 적었고, 또한 기존 대비 디버그에 드는 공수도 크게 줄었다고 느낍니다.
이는 좋은 일이지만, 테스트를 쓰는 이점은 그것만이 아닙니다.
분업 관점에서 보면, 비즈니스 로직 담당자는 테스트를 실행함으로써 자신의 구현이 맞는지를 타인의 구현에 의존하지 않는 형태로 검증할 수 있습니다.
이로 인해 뷰가 미완성이라도 비즈니스 로직의 동작 확인이 가능해, 뷰 구현과 병행·분업해 구현을 진행할 수 있습니다.
또한 이런 병렬 작업 관점 외에 “시프트 레프트” 측면에서도 효과적입니다.
테스트를 쓰지 않으면 버그는 개발 공정 후반, 즉 구현 완료 후 실기기 플레이에서야 발견됩니다.
이 단계의 버그 수정은 다음 이유로 초기 단계 수정에 비해 비용이 훨씬 큽니다.
이런 상황을 막기 위해, 버그 검출과 수정을 프로세스의 더 이른 단계(왼쪽)로 옮기는 “시프트 레프트”가 중요하다고 보고, 그 수단 중 하나로 비즈니스 로직에서는 테스트를 작성했습니다.
덧붙이면, 테스트를 쓰지 않는 이유로 “테스트 쓸 시간이 없다”는 이야기를 자주 듣습니다.
이 주장의 타당성은 프로젝트 컨텍스트에 따라 달라서 본 글에서는 논하지 않겠지만, 위와 같은 시프트 레프트 미실시에 따른 비용 증가가 일어날 수 있는 프로젝트라면 “테스트를 안 써서 더 시간이 없어진다”고도 말할 수 있습니다.
따라서 본 프로젝트에서는 초기부터 테스트를 쓰기로 정하고, 그것이 가능해지는 설계와 워크플로를 구축했습니다.
Unity에서 뷰(GUI) 테스트는 일반적으로 자동화 난이도가 높습니다.
이유는 여러 가지지만, 큰 이유 중 하나는 아래처럼 올바름의 판단에 사람이 필요하다는 점입니다.
외형을 판정해야 하는 것
인터랙션이 필요한 것
이런 것들은 사람의 눈과 손으로 수동 확인을 하지 않으면 정합성 판정이 어렵습니다.
그래서 떠올리기 쉬운 것이 Unity 에디터 또는 실기기에서 실제로 플레이하며 사람이 동작 확인을 하는 방법입니다.
하지만 게임의 뷰는 구성도 상태도 복잡한 경우가 많고, 패턴까지 포함해 플레이 확인하는 것은 매우 비효율적입니다.
예로 아래 그림 같은, 간단한 숍 기능의 1개 상품 뷰를 생각해 봅시다(본 프로젝트와 무관한 뷰입니다).

상품 뷰
구성은 그리 복잡하지 않아 각 요소를 배치하는 것만이라면 금방 구현할 수 있을 것 같습니다.
하지만 이 뷰는 상태가 많고, 아래 요소들을 제어해야 합니다.
배경색 종류(2색 배경을 제어)
아이콘
상품명
수량
가격 타입(달러 등 단위 표시가 바뀜)
가격
배지의 종류/유무
클릭 시 이벤트 발행
이 요소들은 조합해 쓰이므로 패턴은 더 많아집니다.
또 최대 글자수·최소 글자수 등 경계값에서 외형이 깨지지 않는지 확인해야 합니다.
더 나아가 아이콘 이미지 등이 존재하지 않을 때 어떤 동작을 하는지도 확인해 두는 편이 좋을지도 모릅니다.
이런 점을 생각하면, 이것들을 정확히 구현했는지를 실기기에서 디버그하는 것은 역시 비효율적입니다.
또 시프트 레프트 관점에서 보면, 적어도 본 프로젝트에서는 뷰는 비즈니스 로직과 명확히 분리되어 있어야 한다고 말할 수 있습니다.
이렇게 하면 비즈니스 로직 개발이 끝나지 않아도 뷰 개발을 진행할 수 있어 분업 가능성이 높아집니다.
또한 UI 디자이너가 UI를 조정하거나 애니메이션을 붙이거나, 사운드를 붙이는 워크플로도 효율적으로 할 수 있는 형태를 고민해야 합니다.
여기까지를 정리하면, 아래 요구사항을 만족하면 뷰를 효율적으로 개발·동작 확인할 수 있다고 생각됩니다.
이를 달성하기 위해 본 프로젝트에서는 아래 방침으로 구현했습니다.
뷰는 비즈니스 로직(등)을 참조하지 않도록 설계한다
뷰에 더미 데이터를 설정해 개발·동작 확인할 수 있는 씬을 만든다
위 씬은 최소 화면 단위, 숍 상품 예처럼 복잡한 뷰는 더 세밀하게 분할한다
아래는 실제로 이 씬에서 동작 확인을 하는 모습입니다.

Inspector에서 더미 데이터를 넣는 모습
또한 개발 효율을 높이려면, 이 씬을 손쉽게 만들 수 있어야 합니다.
에디터 확장으로, 예를 들어 아래 같은 코드를 쓰기만 해도 위의 Inspector가 초기값과 함께 적당히 표시되도록 했습니다.
csharpusing System; using UnityEngine; namespace Example { [Serializable] internal sealed class RegularShopProductPanelDemoPresenter : MenuDemoPresenter<RegularShopProductPanel> { public void Setup( RegularShopProductPanelColorType colorType = RegularShopProductPanelColorType.Green, string iconResourceKey = "Components/Icon_ShopItem/ShopItem_s_CoinPack_4.png", string productName = "Basket of Coin", int amount = 1, RegularShopProductPriceType priceType = RegularShopProductPriceType.Gem, int priceAmount = 100, string badgeText = "Best Value") { var price = new RegularShopProductDisplayPrice(priceType, priceAmount); view.Setup(colorType, iconResourceKey, productName, amount, price, badgeText, OnClick); return; void OnClick() { Debug.Log($"데모 상품 클릭: {productName} (가격: {priceAmount}, 수량: {amount})"); } } } }
참고로, 더미 서버 응답 JSON을 넣을 수 있게 해서 프레젠테이션 로직 전체를 테스트하는 방법도 생각했습니다.
하지만 그 경우 비즈니스 로직 구현이 뷰 구현의 선행 작업이 되어 분업 가능성이 낮아지므로, 본 프로젝트에서는 채택하지 않았습니다.
다음은 이른바 인게임 이야기입니다.
본 프로젝트에서 아웃게임과 비교했을 때 인게임의 큰 특징은, 인게임은 아웃게임만큼 물량이 많지 않다는 점입니다.
따라서 아웃게임만큼 대규모 인원 병렬 개발을 고려할 필요는 없었습니다.
한편 인게임은 재미와 손맛의 이터레이션을 빠르게 돌릴 수 있어야 합니다.
초기 단계에서는 예상하지 못한 것이 많고, 그런 상태에서 너무 자세히 설계하면 조정 속도가 떨어질 우려가 있었습니다.
이 두 가지 이유로 인게임은 아웃게임보다 가벼운 설계에 머물렀습니다.
어떤 의미에서는 담당자에게 맡겼다고도 할 수 있습니다. 인게임 담당 엔지니어가 정말 노력해 주었습니다. 감사합니다.
다만 가볍게 한다고 해도, 나중에 파탄 나지 않도록 최소한의 설계는 했습니다.
우선 라이프사이클과 실행 순서 제어입니다.
Unity에서는 예를 들어 여러 MonoBehaviour에 Start나 Update를 쓰면 처리 순서가 불명확합니다.
csharppublic class Player : MonoBehaviour { void Update() { } } public class Enemy : MonoBehaviour { void Update() { } }
Script Execution Order 기능으로 관리할 수도 있지만, 세밀히 관리하려면 번거로워지기 쉽습니다.
그러나 실제로는 처리 순서를 정확히 제어해야 하는 장면이 많습니다.
예를 들어 아군과 적이 서로 데미지를 주고받는 배틀 게임에서는 “서로 HP가 동시에(같은 프레임에) 0이 되었을 때”의 동작을 정확히 구현하려면 처리 순서를 엄밀히 제어해야 합니다.
이를 하지 않으면 위 상황에서 “특정 플랫폼에서는 적을 쓰러뜨리는데 다른 플랫폼에서는 아군이 쓰러진다” 같은 매우 골치 아픈 버그가 생깁니다.
또 1프레임만 표시가 어긋나거나, 입력한 처리가 1프레임 늦는 등 재현이 어려운 버그로도 이어집니다.
이런 상황을 막기 위해, 게임 루프와 각 오브젝트의 라이프사이클, 처리 순서를 Unity에 맡기지 않고 관리하는 구조를 구현했습니다.
아래처럼 게임 루프를 돌리고 Tick 메서드에서 각 오브젝트를 적절한 순서로 갱신함으로써 처리 순서를 정확히 제어할 수 있습니다.
csharppublic async UniTask ExecuteAsync() { Enter(); while (true) { Tick(_deltaTimeProvider.GetDeltaTime()); if (_isExitScheduled) { break; } await UniTask.Yield(); } Exit(); }
또 관련 구현으로, 인게임이 가질 수 있는 상태 정의와 상태 전이 구조, 그리고 그 라이프사이클을 관리하는 구현도 함께 했습니다.
그 외에는 다음을 정해두었습니다.
테스트
초기화
개념과 용어 정리
참고로 프로젝트 초기에 편의상 “인게임”, “아웃게임”이라는 말로 영역을 나눴는데, 이는 좋지 않았습니다.
인게임 안에도 “일시정지 다이얼로그” 같은 UI가 표시되는데, 이것이 인게임인지 아웃게임인지 혼란을 불렀기 때문입니다.
기능의 성질로 나눠야 할 것을, 겉보기 단위로 나눈 점은 다음 프로젝트를 위한 반성점 중 하나입니다.
에셋 재작업은 프로젝트 공수를 압박하는 큰 요인입니다.
특히 개발 막바지에 아래 같은 문제가 드러나 대규모 재작업이 발생하는 것은 피해야 합니다.
이 문제들 중에는 폴리곤 수처럼 “사전에 룰을 정해 발생 자체를 막을 수 있는 문제”와, 사양 변경처럼 “발생 자체는 막을 수 없지만 가능한 억제하면서 버퍼도 만들어야 하는 문제” 두 가지가 있습니다.
먼저 전자에 대해서는, 한 번에 그릴 캐릭터 수나 배경 밀도, 이펙트 양 같은 렌더 대상물을 정하고, 버텍스 수나 텍스처 크기, 드로우콜 수 등 정량적 레귤레이션으로 떨어뜨리면 프레임 드롭이나 크래시를 막을 수 있습니다.
다음으로 후자에 대해서는, 사양 변경이나 연출 리치화 요구는 개발을 진행하면 반드시 발생하며, 이는 제품 가치를 높이기 위해 필요한 일이기도 합니다.
하지만 “재미를 위해서라면 무엇이든 얼마든지 해도 된다”로 가면, 최악의 가정이긴 하지만 많은 재작업이 발생하고, 그 결과 데스마치에 빠져 건전한 위기감을 가진 멤버가 이탈하고, 더 나아가 무리를 밀어붙여 극복하는 것을 미덕으로 삼는 조직 체제나 문화가 남아 처음으로 돌아가는 악순환이 생길 수 있습니다.
이런 사태를 막기 위해 프로젝트 초기에는 다음 움직임이 필요합니다.
정할 수 있는 것은 가능한 한 모두 정해둔다
그 위에서 바로 정할 수 없는 것은, 나중에 무엇이 어느 정도 바뀔 수 있는지 상정하고 버퍼를 둔다
요컨대 재작업을 막기 위해 불확실성에 가능한 한 대응해두는 움직임이 중요하다는 것입니다.
이를 위해 본 프로젝트에서는 LookDev로서 검증 페이즈를 초기에 두고, 검증과 합의 형성을 진행했습니다.
LookDev라는 공정 정의는 프로젝트마다 조금 다를 수 있지만, 본 프로젝트에서는 “처리 부하 등 비기능 요구도 충족한 상태에서 최종적인 외형을 정하는 공정”으로 정의했습니다.
구체적으로는 다음을 했습니다.
동작을 보장하는 모바일 단말 기준 결정
프로토타입 제작과 그림 만들기
성능 검증
워크플로 확립
위 SIRIUS는 제가 소속된 코어테크 그래픽스 팀이 개발하는, 여러 프로젝트에서 범용으로 쓸 수 있는 스타일라이즈드 렌더링 시스템입니다.
자세한 내용은 아래 강연을 참고해 주세요.
또 버텍스 수나 텍스처 크기 같은 레귤레이션은 정하고 끝이 아니라, 이를 지킬 수 있는 워크플로를 갖추는 것도 중요합니다.
지킬 수 없는 룰은 효과가 없을 뿐 아니라, 룰을 정하는 공수만큼 개발 효율을 떨어뜨리기 때문입니다.
본 프로젝트에서는 임포트 파이프라인에서 Unity에 넣기 전에 텍스처를 컨버트하는 등 대응을 하고 있어 출번은 없었지만, 지킬 수 있는 워크플로를 만드는 도구로서, 정한 레귤레이션을 체크할 수 있는 Asset Regulation Manager라는 툴을 코어테크에서 릴리스하고 있습니다.

Asset Regulartion Manager 화면
이는 OSS로 공개하고 있으니, 괜찮으시면 써보세요.
개발을 진행하면 예상한 프레임레이트가 나오지 않는 등 런타임 성능이 문제가 되는 경우가 있습니다.
이런 런타임 성능은 실기기에서 플레이하면 금방 알 수 있다는 의미에서, 과제로 올라오기 쉬운 편이라고 할 수 있습니다.
반면 “Unity 에디터 자체의 성능 튜닝”은 과제로 올라오기 어렵지만, 개발 효율 관점에서는 매우 중요합니다.
예를 들어 개발이 진행되면 아래 시간이 늘어나는 일이 흔합니다.
이들은 개발자가 기다리면 끝나는 문제이기에 “좀 느리긴 한데 어쩔 수 없지”로 수정 우선순위가 내려가거나 방치되기 쉽습니다.
하지만 이런 대기 시간은 프로젝트 전체로 보면 무시할 수 없는 시간이 될 수 있고, 또한 대기 시간 자체 외에도 대기 때문에 집중이 끊기는 등 보이지 않는 비용이 발생합니다.
그래서 본 프로젝트에서는 대기 시간이 가능한 늘지 않도록, 프로젝트 초기부터 개발 중에 걸쳐 몇 가지 대응을 했습니다.
먼저 한 것은 어셈블리 분할입니다.
Unity 기본 설정에서는 모든 스크립트가 Assembly-CSharp.dll에 모이는데, 이렇게 되면 한 줄만 바꿔도 프로젝트 전체 재컴파일이 돌게 됩니다.
Assembly Definition File로 어셈블리를 분할하면, 해당 어셈블리 및 그 어셈블리를 참조하는 어셈블리만 컴파일됩니다.

Assembly Definition File의 Inspector
어셈블리는 구현이 진행된 뒤에는 분할이 어렵기 때문에, 프로젝트 초기 설계 단계에서 분할해두어야 합니다.
다만 너무 쪼개면 관리가 어려워지므로, 적절한 입도를 유지하는 것도 중요합니다.
또 “Compilation Visualizer for Unity”라는 OSS를 쓰면 어셈블리별 컴파일 시간을 계측할 수 있어 추천합니다.
다음은 에셋 임포트 시간 등입니다.
본 프로젝트는 출시 시점에 3D 모델이 1000개 이상 있었고, 각 모델이 텍스처를 2장씩 사용했습니다.
이 텍스처는 4096 x 4096px로 제작됐지만, 앞서 LookDev 결과, 런타임에서 쓰는 텍스처는 512 x 512px로 하기로 했습니다.
Unity에서는 큰 텍스처를 임포트해도, 임포트 설정에서 실제 앱에 넣을 사양에 맞춰 리사이즈 및 압축을 할 수 있습니다.

임포트 설정 화면
하지만 애초에 Unity에 필요 이상으로 큰 텍스처를 넣으면, 임포트 시간/플랫폼 전환 시간, 버전 관리 툴로 원격 텍스처를 가져오는 시간 등 모든 시간이 늘어납니다.
이를 막기 위해 Unity Accelerator를 쓰거나, 필요한 파일만 취득 가능한 버전 관리 툴을 쓰는 방법도 있습니다.
이번 프로젝트에서는 Unity에 임포트하기 전에 리사이즈 처리하는 파이프라인을 만들어, 필요한 최소 크기의 텍스처만 임포트하기로 했습니다.
그 결과 임포트 시나 원격 취득 시 등의 불필요한 시간을 줄일 수 있었습니다.
참고로 Unity 2021에서 이 주변의 임포트 시간 고속화 및 분석 도구가 들어갔으니, 알아두면 편리합니다.
또 개발이 진행되어 여러 엔지니어가 여러 에디터 확장 툴을 만들기 시작하면, 그것들로 인해 성능이 떨어질 수 있습니다.
Unity에는 런타임 성능을 측정하는 Profiler라는 툴이 있는데, 이를 Edit Mode에서 켜면 에디터 성능 측정도 가능합니다.
본 프로젝트에서는 개발 중 몇 차례 이를 써서 성능을 체크했습니다.

Profiler 화면
마지막은 도메인 리로드 비활성화 대응입니다. 이는 다른 엔지니어 분이 제안·구현해 주었습니다. 감사합니다.
Unity에서는 개발 중 “Play 버튼을 눌러 동작 확인”을 매우 자주 하는데, 이때 발생하는 대기 시간은 개발이 진행될수록 길어집니다.
Unity 설정에서 도메인 리로드를 비활성화하면 이때의 대기 시간을 줄일 수 있습니다.
주의점으로, 도메인 리로드를 비활성화하면 static 메모리 영역이 클리어되지 않으므로, 아래처럼 명시적으로 클리어 처리를 해야 합니다.
csharpusing UnityEngine; public class Game { public static int Score; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Init() { Score = 0; } }
본 프로젝트는 앞서 말했듯 static을 원칙 금지로 했기에, 추가 공수 없이 이 구조를 도입할 수 있었다고 생각합니다.
개발 효율을 생각할 때, 앞 절의 Unity 에디터 대기 시간처럼 간과되기 쉬운 문제로 “과도한 견적에 쓰는 공수”가 있습니다.
우선 많은 프로젝트에서, 구현이 견적 시간을 초과하는 현상은 일상적으로 발생합니다.
이는 반드시 엔지니어의 능력 부족이라기보다, 작업에 포함된 불확실성이 원인인 경우가 많습니다.
예를 들어 “써본 적 없는 라이브러리 도입”이나 “전례 없는 기능 구현” 등은 실제로 손을 움직이기 전에는 정확한 공수를 알 수 없습니다.
정확한 공수를 모른 채 견적을 내고 커밋하면 위처럼 지연으로 이어지기 쉽습니다.
만약 이를 실태에 기반해 견적한다면 “최단 3일이지만 최장 15일” 같은 넓은 범위를 잡게 됩니다.
범위가 넓어 보이지만, “예기치 못한 사태에 드는 공수”와 “발생 확률”의 그래프는 롱테일 분포이므로, 최악의 경우를 올바르게 상정하면 최장은 더 길게 잡아야 하는 경우도 많습니다.
하지만 견적의 목적은 스케줄 수립이나 리소스 배분인 경우가 많아, 이런 넓은 범위 견적은 정보 가치가 낮다고 판단되기 쉽습니다.
그래서 억지로 “정밀도”를 올려 “8일~10일” 같은 수치를 내기도 하지만, 이는 정밀도를 올리는 것이 아니라 불확실성이라는 리스크를 숨기는 것에 불과합니다.
이것은 더 이상 견적이라고 부를 수 없고, 희망사항입니다.
상세한 사전 조사를 하면 더 정확한 견적이 나오지만, 그것 자체에도 공수가 들고, 정확히 견적할 수 있을 즈음에는 구현이 거의 끝나 있는 경우도 흔하므로, 해야 할지는 목적에 달렸다고 할 수 있습니다.
또 미래에 대한 견적일수록 정밀도는 떨어집니다. 이는 유명한 배리 뵘 씨의 불확실성 콘이 보여주는 바입니다.
“약속은 개발을 늦춘다”는 이야기도 있습니다.
특히, 이제는 구식일지도 모르지만, 작업마다 세밀하게 엄격한 공수 관리를 하고 납기에 커밋시키는 프로젝트 매니지먼트 방식에서 이런 사태가 생기기 쉬운 것 같습니다.
그렇다고 견적을 아예 안 하는 게 좋냐 하면 그렇지도 않습니다.
스케줄/인원 계획/경영 판단 등을 가능한 정확히 하고 싶다면, 역시 견적은 필요합니다.
정확하지 않더라도 대략 파악하고 싶다면, 최소한 대략적인 견적은 필요합니다.
이처럼 무엇을 어디까지 어느 정도 정밀도로 견적할지는 생산성 관점에서 매우 어려운 문제이며, 쉽게 정할 수 있는 것이 아닙니다.
하지만 아무것도 정하지 않고 맹목적으로 진행하면 “돌아보니 견적이나 스케줄 조정, 그에 따른 회의에 매우 많은 시간을 쓰고 있었다”는 사태가 될 수 있습니다.
본 프로젝트에서는 위 내용을 바탕으로 아래 점에 유의했습니다.
견적의 목적을 확인하고 필요한 부분만 견적한다
해보기 전엔 모르는 부분이 있음을 인정하고, 무리한 숫자 맞추기에 쓸데없는 시간을 쓰지 않는다
먼 미래의 견적은 하지 않고, 하더라도 대략 ‘예상’ 정도로만 한다
구현 이미지가 구체적으로 떠오르지 않으면, 바로 견적하지 말고 조사 작업을 먼저 한다
견적할 때는 구체적으로 작업 이미지가 그려지는 입도(30분~3시간 정도)까지 분해해서 견적한다
결과적으로 견적 자체의 공수나 스케줄 재작성 공수, 그에 따른 회의 공수 등 많은 공수를 줄이고, 그 시간을 구현에 쓸 수 있었다고 생각합니다.
이제 글이 길어졌으니, 더 쓰고 싶은 것이 있어도 마지막으로 툴의 생산성에 대해 언급하며 마무리하겠습니다.
게임 개발 과정에서는 다양한 툴을 개발합니다.
그중에는 아래처럼 엔지니어가 작업을 효율화하는 툴뿐 아니라, 기획자/디자이너/QA 등 엔지니어가 아닌 직무의 분들이 쓰는 툴도 많습니다.
더 많은 멤버가 더 많은 횟수로 사용하는 툴일수록 UX를 다듬지 않으면, 프로젝트 전체 관점에서 무시할 수 없는 공수를 낭비하게 됩니다.
예를 들어 개발 중 사용하는 디버그 메뉴를 생각해 봅시다.
“게임을 클리어 상태로 만든다”, “소지금을 MAX로 한다”, “무적 모드를 ON으로 한다” 같은 기능입니다.
이런 디버그 메뉴는 프로젝트에 따라 다르지만, 개발이 진행되면 수백 개 규모가 될 수 있습니다.
극단적으로 나쁜 예로, 이 메뉴들이 의미 있는 묶음 없이 하나의 화면에 수백 개 버튼으로 나열되어 있다면, 테스터는 매번 테스트 때마다 스크롤하며 목적 메뉴를 찾아야 합니다.
한 번당 수십 초~수 분의 손실이 생기고, 이를 하루에 1인당 수십 번 반복하게 됩니다.
이를 막기 위해 디버그 메뉴는 구조화하거나 검색 기능을 넣을 필요가 있습니다.
본 프로젝트에서는 이를 위해 Unity Debug Sheet를 도입했습니다.

Unity Debug Sheet 샘플 화면
이는 제가 개인적으로 릴리스한 OSS입니다.
홍보처럼 보일 수 있지만, 목적이 제대로 달성되기만 하면 무엇이든 좋습니다.
참고로 구조화나 검색 기능의 이점으로 특정 항목이 “없다”는 것을 알 수 있다는 점도 있습니다.
목표 디버그 항목이 아직 없으면 담당 엔지니어에게 항목 추가를 의뢰해야 하는데, 그 항목이 “존재하지 않는다”는 사실이 명확하지 않으면 심리적으로 의뢰하기가 어렵습니다.
구체적으로는 다음 같은 비효율이 생깁니다.
구조화나 검색 기능이 있으면 명확히 “없다”는 것을 알 수 있어 이런 일을 막을 수 있습니다.
일찍 정리해두지 않으면, 프로젝트가 진행될수록 항목 존재 여부의 증명이 악마의 증명처럼 되어가므로, 빠른 정리가 중요합니다.
또 이런 디버그 메뉴를 쓰기 쉽게 하려면 구조화뿐 아니라, 단어 선택도 매우 중요합니다.
예를 들어 “젬을 부여하는 디버그 메뉴” 이름이 “Execute AcquireGemUseCase”라면 어떨까요.
구현한 엔지니어 본인에게는 이해되겠지만, 다른 사람 특히 비엔지니어에게는 매우 이해하기 어려운 항목명입니다.
“젬을 늘리기” 같은 메뉴명이 누구에게나 훨씬 직관적입니다.
좋은 UX와 나쁜 UX 차이에 대해서는 아래 글이 매우 참고가 됩니다.
엔지니어(물론 저도 포함)가 자주 해버리는 구현 사정 중심 UI를 피하기 위한 관점이 담겨 있습니다.
또한 디버그 메뉴처럼 런타임에서 쓰는 툴뿐 아니라, Unity 에디터에서 쓰는 툴도 마찬가지입니다.
본 프로젝트 예로, 마스터 데이터 관리 툴을 아래 같은 에디터 확장으로 만들었습니다.

마스터 데이터 관리 툴 화면
큰 실수 없이 운용되고 있어 나쁘지 않다고 생각합니다.
본 프로젝트에서는 개발 효율이라는 관점 아래, 프로젝트 기동기부터 다양한 시도를 했습니다.
물론 이번 내용이 모든 프로젝트에 그대로 맞지는 않겠지만, 참고가 되는 부분이 있으면 좋겠습니다.
마지막으로 홍보입니다만, 제가 소속된 주식회사 사이버에이전트 SGE 코어기술본부(코어테크)에서는 아래 엔지니어를 모집하고 있습니다.
코어테크의 일에 대해서는 지난주 Automaton에서 인터뷰해 주신 글도 있으니, 괜찮으시면 참고해 주세요.
또 개인적으로 개발 기간 약 1년의 단기 개발 프로젝트는, 과제 발견과 실천의 사이클을 돌리기 쉬워 실제로 해보니 매우 매력적으로 느꼈습니다.
당사는 대규모 프로젝트도 많지만, 본 프로젝트 같은 규모의 프로젝트도 개발하고 있고, 앞으로의 주력 영역 중 하나가 되고 있습니다.
코어테크에 한정하지 않고 엔지니어를 모집하고 있습니다(그리고 전혀 부족합니다). 관심 있는 분은 아래 페이지도 참고해 주세요.