언리얼 기반 개발과 깃은 잘 어울리지 않는 것 같음
이전까지 언리얼 개발은 SVN이나 퍼포스를 사용했습니다. 둘 다 중앙 집중식 형상관리시스템으로 엔지니어가 아닌 사람 - 게임디자인 - 입장에서 큰 불만이 없었습니다. SVN에서는 브랜치 스위칭에 시간이 많이 걸리곤 했고 퍼포스는 종종 리비전이 부정확하게 다운로드 되거나 브랜치에 로컬 스토리지가 엄청나게 많이 필요하곤 했지만 트러블 슈팅이 어렵지 않아 나쁘지 않았습니다. 처음으로 언리얼 개발에 깃을 사용하려 할 때 잠깐 걱정했지만 또 이럴 때가 아니면 대규모 언리얼 개발에 깃을 언제 써볼까 싶어 반대하지 않았습니다. 그런데 시간이 지날 수록 언리얼과 Git은 기술적으로 함께 사용할 수 있지만 서로 잘 어울리지는 않는다고 생각하게 되었습니다.
엔지니어와 게임디자이너가 같은 리파지토리를 사용하더라도 파일시스템 상 서로 겹치는 파일을 사용할 가능성이 거의 없습니다. 엔지니어가 소스 디렉토리의 파일을 주로 사용하고 CI가 바이너리 디렉토리를 주로 사용한다면 게임디자이너는 주로 컨텐츠 디렉토리, 그 중에서도 데이터 디렉토리를 가장 자주 사용합니다. SVN이나 퍼포스에서는 파일시스템 상 서로 겹치지 않는 위치를 편집했다면 서로를 신경 쓰지 않고 업데이트(풀)나 커밋(커밋+풀)을 하는데 제한이 없습니다. 리파지토리를 업데이트 한 다음 데이터를 편집하고 나서 이를 바로 커밋할 수 있고 로컬에 편집한 파일이 커밋 되지 않은 상태이더라도 바이너리 디렉토리만 업데이트 할 수도 있습니다.
깃에서는 일단 로컬에 커밋되지 않은 파일이 있으면 무조건 이를 정리하지 않으면 업데이트조차 할 수 없습니다. 업데이트 하기 위해 로컬 파일을 커밋하더라도 이를 중앙에 반영하려면 반드시 업데이트를 통해 최신인 상태에서만 반영할 수 있습니다. 깃의 파일 데이터베이스 구조 상 이 동작이 당연하다는 점은 이해하지만 이전 경험에 비해 현재 경험이 엔지니어가 아닌 사람 입장에서 훨씬 불편하고 매 업데이트마다 생산성을 크게 떨어뜨립니다.
또한 깃은 업데이트(풀) 할 때 리파지토리의 일부를 선택해서 업데이트 할 수 없습니다. 로컬에서 데이터를 수정해 테스트 하다가 새 기능을 사용하기 위해 최신 바이너리를 업데이트 해야 할 경우 반드시 로컬 데이터를 정리해야만 합니다. 커밋하거나 스태시하거나 해서요. 바이너리 뿐 아니라 다른 사람이 수정한 데이터를 받아 함께 테스트에 포함할 때도 마찬가지입니다. 또한 업데이트에 바이너리가 포함되지 않더라도 파일이 사용 중이면 업데이트(풀)에 실패하므로 파일이 사용 중이든 사용 중이 아니든 반드시 작업 환경을 모두 종료해야 하는데 매 업데이트마다 이 짓을 반복해야 하므로 이전에 비해 이터레이션에 훨씬 많은 시간이 걸립니다.
머지 할 수 없는 바이너리 파일을 수정할 때 여러 사용자가 동시에 접근하는 상황을 형상관리시스템 수준에서 막지 못합니다. 위에서 언급한 데이터 파일은 때로는 엑셀 형식이지만 때로는 위젯 블루프린트나 데이터에셋이기도 합니다. 특히 위젯 블루프린트나 데이터에셋은 확장자가 *.uasset
인데 이들은 종류에 따라 비교할 수는 있지만 머지 할 수는 없습니다. SVN은 이런 상황에서 파일에 락을 걸어 다른 사람이 체크아웃 하지 못하게 할 수 있고 퍼포스에서도 한 사람이 체크아웃 하면 다른 사람이 같은 파일을 체크아웃 할 수 없어 머지 불가능한 파일의 충돌을 방지합니다.
반면 깃은 서로 다른 사람이 동시에 머지 불가능한 파일을 수정하는 상황을 막지 않습니다. 두 사람이 같은 파일을 수정하면 로컬에서 커밋했더라도 한 사람이 푸시 하고 나면 다른 사람은 충돌을 해결할 때까지 아무 작업도 할 수 없습니다. 하지만 말이 좋아 충돌 해결이지 대상이 바이너리인 이상 나중에 푸시 하는 사람이 작업을 버리거나 두 사람 중 작업 내용이 더 단순한 사람이 다시 작업 해야 합니다. Git LFS에서 SVN과 비슷한 락을 제공하지만 여러 브랜치에서 예상대로 동작하지 않습니다. 특히 브랜치 비용이 비싼 SVN이나 퍼포스와 달리 Git은 브랜치를 적극적으로 사용하는 워크플로우인데 브랜치 상에서 제대로 동작하지 않는 락은 별 의미가 없습니다.
이런 충돌을 완화하려면 충돌이 일어나는 에셋을 적극적으로 분리해야 합니다. 위 사례에서 게임디자인과 아트에서 같은 위젯 블루프린트를 편집해야 하면 서로의 목적에 따라 에셋을 분리해 같은 파일을 편집하지 않도록 하곤 합니다. 서로 다른 여러 부서에서 레벨을 편집해야 한다면 목적이나 부서 별로 서브레벨을 나눠 서로 파일이 충돌하지 않게 하고 또 게임디자인끼리 같은 데이터에셋을 자주 편집한다면 서로의 목적에 맞게 에셋을 분리해 각자 편집하게 하고 빌드타임이나 런타임에 이들을 한번에 읽어 반영할 수 있습니다.
규모가 커질수록, 작업자가 늘어날수록 이런 방법을 사용해야 하지만 이런 방법을 통한 개입이 늘어날수록 서로 다른 데이터에셋에 중복 데이터가 들어있지만 이를 파악할 수 없어 문제 해결에 어려움을 겪거나 한 가지 온전한 동작을 만들어내기 위해 서로 다른 여러 파일의 작용을 이해해야 하는 식으로 개발 난이도가 올라갑니다. 게다가 규모가 충분히 커지기 전부터 이런 접근을 요구하는 깃과 언리얼 환경의 조합은 엔지니어가 아닌 사람 입장에서 그리 달갑지 않습니다.
일단 프로젝트는 시작됐고 이런 난처함에도 불구하고 Git을 사용하게 될 겁니다. 기왕 이전에 비해 훨씬 불편하게 느끼는 환경에서 개발을 하고 있으니 시행착오를 겪어 그럭저럭 납득할만한 개발 시나리오를 확립하거나 다음번 언리얼 프로젝트에 깃을 도입하려고 하면 적극적으로 가로막거나 중 어느 한쪽으로 결론을 낼 작정입니다.