버전 관리 및 패치 적용
언리얼 프로젝트의 변경 이력과 릴리스 버전을 관리하고 전체·증분 패치를 안전하게 생성하고 배포하는 전략을 익힙니다.
유지보수의 흐름을 잡았다면, 이제 그것을 실제 배포 체계로 이어야 합니다. 개발하는 동안의 변경 이력을 체계적으로 남기지 않으면, 함께 작업할 때 충돌이 되풀이되고 한 번 고친 버그가 다시 살아나는 회귀(regression)가 생깁니다. 또 패치를 전달하는 체계가 약하면, 업데이트할 때마다 플레이어가 게임을 다시 설치하거나 세이브가 깨지는 일이 생깁니다.
이 글에서는 버전 관리 시스템(Version Control System, VCS)이 왜 필요한지 정리하고, 언리얼 프로젝트에서 주로 쓰는 Git, Perforce, SVN을 비교한 뒤 에디터와 연결하는 방법을 살펴봅니다. 그다음 pak 기반의 콘텐츠 패치를 만들고 적용하는 과정을 따라가며 패치의 크기를 계산해 보고, 마지막으로 버전 관리와 패치를 함께 운영할 때 고려할 점을 정리합니다. 이 구조를 갖추면 개발 단계와 운영 단계를 하나의 일관된 과정으로 묶을 수 있습니다.
버전 관리가 필요한 이유
버전 관리 시스템은 개발 팀이 코드와 콘텐츠의 변경을 추적하고 관리하는 도구입니다. 누가 언제 무엇을 바꿨는지 기록하므로, 문제가 생겼을 때 원인을 쉽게 찾고 되돌릴 수 있습니다. 여러 개발자가 같은 파일을 고쳐도 충돌을 줄이고 변경을 합칠(merge) 수 있게 도와줍니다.
프로젝트를 안전하게 저장해 두므로, 실수로 파일이 손상되거나 지워져도 이전 버전으로 되살릴 수 있습니다. 특정 버전의 코드로 돌아가 그 빌드를 다시 만들거나, 문제가 없던 안정된 버전으로 되돌릴 수도 있습니다. 새 기능을 만들거나 버그를 고칠 때는 메인 개발 줄기에 영향을 주지 않는 별도의 브랜치(branch)에서 안전하게 작업할 수 있습니다. 에셋의 버전 관리는 에셋의 버전 관리에서도 다뤘습니다.
주로 쓰는 버전 관리 시스템
Git
Git은 분산 버전 관리 시스템으로, 일반적인 전체 복제(clone)를 하면 개발자마다 이력 전체의 사본을 가집니다. 다만 얕은 복제(shallow clone)나 부분 복제, Git LFS 객체의 다운로드 범위는 다를 수 있습니다. 빠르고 유연하며, 브랜치를 만들고 합치는 일에 특히 강합니다.
장점은 오프라인으로 작업할 수 있고 작업 속도가 빠르며, 커뮤니티가 활발하고 Sourcetree, GitKraken, GitHub Desktop 같은 GUI 도구가 많다는 것입니다. 단점은 언리얼 에셋 같은 큰 바이너리 파일을 다루기가 상대적으로 비효율적이라는 것입니다. Git LFS(Large File Storage)로 보완할 수 있지만 설정이 필요하고, 이력이 길어지면 저장소가 매우 커질 수 있습니다. 중소 규모의 프로젝트나 C++ 코드 중심의 프로젝트에 알맞으며, Git LFS를 설정하면 언리얼 에셋도 관리할 수 있습니다.
Perforce (Helix Core)
Perforce의 서버 제품은 지금 Perforce P4라는 이름으로 나오며, 제목의 Helix Core는 예전 이름입니다. Perforce는 모든 변경을 중앙 서버로 관리하는 중앙 집중식 버전 관리 시스템이며, 큰 바이너리 파일을 다루는 데 잘 맞춰져 있습니다. 에픽게임즈가 직접 쓰고 권장하며, 대규모 프로젝트와 수많은 언리얼 에셋을 관리하는 데 뛰어납니다. 파일을 고치기 전에 체크아웃(checkout)하고 끝나면 제출하는 방식이 분명해 바이너리 파일의 충돌을 줄여 주고, 세밀한 권한 관리를 제공합니다.
단점은 서버에 장애가 생기면 동기화와 제출, 잠금 같은 작업이 제한된다는 것입니다. 이미 받아 둔 파일을 오프라인으로 편집할 수 있는지는 작업 정책과 도구 설정에 따라 다릅니다. 라이선스 비용이 들 수 있지만, 작은 팀은 무료 버전을 쓸 수 있습니다. 콘텐츠가 많은 대규모 상업 게임 프로젝트에 가장 알맞습니다.
SVN (Subversion)
SVN은 Git보다 오래된 중앙 집중식 버전 관리 시스템으로, Perforce처럼 중앙 서버를 씁니다. 설정이 비교적 간단하고, Git LFS 같은 추가 설정 없이도 바이너리 파일을 관리할 수 있습니다. 하지만 브랜치를 만들고 합치는 기능이 Git보다 약하고, 대규모 프로젝트에서는 성능 문제가 생길 수 있습니다. 작은 팀이나 간단한 프로젝트에서 Perforce를 갖추기 어려울 때 대안으로 쓸 수 있습니다.
에디터와 버전 관리 연결하기
에디터의 리비전 컨트롤(Revision Control) 기능으로 제공자와 외부 클라이언트를 연결합니다. 예전 버전의 에디터와 일부 문서에서는 이 기능을 소스 컨트롤(Source Control)이라고 부릅니다. Perforce와 SVN의 지원, Git 플러그인의 설치와 기능 범위는 쓰는 버전에서 확인합니다.
먼저 Edit 메뉴의 Plugins에서 쓸 버전 관리 시스템의 플러그인, 예를 들어 Perforce나 Git을 켭니다. 그다음 에디터의 리비전 컨트롤 메뉴에서 제공자와 연결 설정을 엽니다. 메뉴의 위치는 버전에 따라 다릅니다. 선택한 시스템에 따라 서버 주소와 포트, 사용자 이름 같은 정보를 넣고 연결합니다. Perforce라면 P4V 클라이언트를 설치하고 워크스페이스를 설정한 뒤 언리얼 엔진에서 그 워크스페이스에 연결합니다. Git이라면 저장소를 초기화하고, Git LFS로 .uasset 같은 큰 파일을 추적하도록 .gitattributes 파일을 구성합니다.
연결이 되면 콘텐츠 브라우저의 에셋 아이콘에 잠김, 수정됨, 추가됨 같은 상태 아이콘이 표시됩니다. 파일을 고치기 전에는 콘텐츠 브라우저에서 그 파일을 오른쪽 클릭해 리비전 컨트롤 항목(버전에 따라 Source Control)의 체크아웃을 골라 편집할 수 있게 엽니다. 바이너리의 동시 편집을 막는 배타적 잠금이 걸리는지는 Perforce의 typemap 같은 정책도 확인합니다. 수정을 마치면 같은 메뉴에서 제출(체크인)하거나 커밋해 변경을 반영하며, 이때 무엇을 바꿨는지 분명히 설명하는 메시지를 남깁니다.
pak 기반 콘텐츠 패치 만들기
출시한 뒤 패치와 업데이트를 효율적으로 배포하는 것은 플레이어 경험에 바로 이어집니다. 여기서는 pak 기반의 콘텐츠 패치를 다룹니다. 아이오 스토어나 각 스토어, 콘솔의 패치 방식은 그 경로의 지침을 따르며, 실행 코드의 변경은 콘텐츠 pak만으로 전달할 수 없다는 점을 먼저 기억해 둡니다.
기준 릴리스 만들기
패치는 이미 배포한 버전을 기준으로 바뀐 것만 담으므로, 먼저 기준이 되는 릴리스를 만들어 보관해야 합니다. 툴바의 Platforms 메뉴에서 프로젝트 런처(Project Launcher)를 열고, 사용자 지정 실행 프로필(Custom Launch Profile)에서 대상 플랫폼과 Shipping, 정해진 목록대로 쿠킹하는 Cook by the Book을 설정합니다. 그리고 Release/DLC/Patching 항목에서 릴리스를 만드는 옵션을 켜고 이름을 정해 패키징합니다. 그러면 Releases/[릴리스 이름]/[플랫폼] 폴더에 에셋 레지스트리와 pak 파일이 만들어지는데, 이것과 원래 쿠킹한 콘텐츠, 빌드 설정을 함께 보관합니다. 이 릴리스 이름은 엔진의 Build.version과는 다른 것입니다.
바꾼 뒤 패치 만들기
기준 릴리스를 만든 시점의 소스에는 버전 관리 시스템에서 그 시점을 나중에 다시 찾을 수 있게 표시하는 태그(tag)를 붙여 둡니다. 그다음 그 기준 태그 이후에 바꾼 에셋과 코드를 나눠 둡니다. 직렬화 방식이나 엔진, 쿠킹 설정을 바꿨다면 기존 콘텐츠와 호환되는지도 확인해야 합니다.
패치를 만들 때는 같은 사용자 지정 실행 프로필에서 Cook by the Book과 대상 맵, 플랫폼을 확인합니다. 그리고 Release/DLC/Patching에서 Release Version This is Based On에 앞에서 보관한 기준 릴리스의 이름을 넣고 Generate Patch를 고릅니다. 별도의 에셋 레지스트리 버전 숫자를 입력하는 절차로 이것을 대신하지 않습니다.
패치에 무엇이 들어가나
패치의 변경 단위는 .uasset 같은 에셋 패키지입니다. 그래서 패키지 안의 값 하나만 바뀌어도 그 패키지 전체가 패치에 들어가므로, 패치가 늘 작다고 보장할 수 없습니다. 만든 패치는 원래의 설치본에 적용해 보고, 로드 우선순위와 세이브의 호환성을 시험합니다.
계산으로 확인해 봅니다. 압축과 암호화, 컨테이너의 헤더를 빼고, 쿠킹한 패키지 A가 1 MiB, B가 8 MiB, C가 16 MiB라고 해 봅시다. A의 값 하나와 B의 재질 연결만 바꾸고 C는 바꾸지 않았다면, 이 비교 모델에서 패치에 들어가는 것은 A와 B의 전체 패키지로 합계 1 + 8 = 9 MiB입니다. 바뀐 속성의 몇 바이트만 더하는 방식이 아닙니다. 실제로 내려받는 크기와, 설치하는 동안 순간적으로 필요한 공간은 배포 시스템에서 따로 측정합니다.
패치 적용하기
만든 패치를 플레이어의 게임에 적용하는 방법은 여러 가지입니다.
개발 중에 확인할 때는 패치 pak을 그 빌드가 pak을 찾는 폴더에 직접 넣고, 이름이 _p.pak으로 끝나는 패치가 우선순위가 높아 원래의 콘텐츠를 덮어 읽는지 확인합니다. 하지만 실제 사용자에게 배포할 때는 설치 프로그램이나 스토어의 무결성 확인과 업데이트 절차를 씁니다.
대부분의 상업 게임은 자체 런처나 Steam, Epic Games Launcher 같은 플랫폼의 런처를 씁니다. 런처는 게임 서버나 콘텐츠 전송 네트워크(CDN)에서 최신 패치 파일을 내려받아, 플레이어의 설치 경로에 자동으로 놓아 적용합니다. 언리얼 엔진의 UnrealFrontend는 개발용 빌드와 배포 도구일 뿐, 사용자가 패치를 받는 런처나 CDN을 제공하지는 않습니다.
게임을 다시 시작하지 않고 일부 데이터만 바꾸는 방법도 있습니다. 주로 서버 기반 게임에서 밸런스 수치나 이벤트 데이터를 바로 바꿀 때 쓰며, UOnlineHotfixManager 같은 실행 파일이 아닌 데이터를 적용하는 경로로 만듭니다. 바뀐 pak이나 이미 불러온 레벨은 다시 시작하거나 다시 불러와야 할 수 있으며, 에디터에서 코드를 바로 반영하는 핫 리로드(Hot Reload)나 라이브 코딩(Live Coding)과는 다른 기능입니다.
함께 운영할 때 고려할 것
모든 변경에는 분명하고 간결한 커밋이나 제출 메시지를 남겨 이력을 쉽게 파악하게 합니다. 새 기능 개발, 버그 수정, 출시 준비처럼 목적에 따라 Git Flow 같은 알맞은 브랜치 전략을 쓰고, 메인 브랜치는 늘 안정된 상태로 유지합니다. 주요 릴리스와 패치 버전에는 태그(tag)를 붙여 그 시점의 코드를 쉽게 찾을 수 있게 합니다.
빌드는 자동화합니다. Jenkins나 TeamCity 같은 지속적 통합(CI) 서버에서 코드를 빌드하는 언리얼 빌드 툴(UBT)과, 쿠킹과 패키징을 하는 언리얼 자동화 툴(UAT)이나 BuildGraph를 호출해 자동화하면 실수가 줄고 빌드 시간도 짧아집니다. UBT 자체가 CI 서버인 것은 아닙니다. 패치를 적용하기 전에는 QA 팀이나 베타 시험자에게 시험용 빌드를 배포해 충분히 시험합니다.
패치의 크기는 되도록 작게 만들어 플레이어의 다운로드 시간과 데이터 비용을 줄입니다. 플레이어의 네트워크가 불안정할 수 있으므로, 패치 다운로드가 실패했을 때 다시 시도하는 로직도 만듭니다.
저장소와 릴리스 보관
저장소에는 .uproject와 Content, Config, Source, 그리고 필요한 Plugins와 빌드 원본을 관리하고, Saved, Intermediate, DerivedDataCache 같은 다시 만들 수 있는 폴더와 구분합니다. 바이너리 에셋의 잠금과 LFS 정책은 에셋을 가져오기 전에 정해 두고, LFS 포인터만 있고 실제 파일이 없는 상태가 생기지 않았는지 확인합니다.
소스의 태그와 엔진, SDK의 버전, 쿠킹 규칙, 기준 릴리스, 패치 결과물을 서로 연결해 기록합니다. 핫픽스는 개발 줄기에도 합쳐서, 다음 버전에서 같은 버그가 다시 생기지 않게 합니다. 그리고 이전 설치본에 패치를 적용하는 경우와, 실패했을 때 되돌리는 경우, 저장 데이터의 호환성을 함께 확인합니다.
정리
- 버전 관리는 변경 이력을 남겨 되돌리고 함께 작업하게 해 주며, 브랜치로 메인 줄기에 영향을 주지 않고 작업합니다.
- Git은 분산형으로 코드에 강하고 에셋에는 LFS가 필요하며, Perforce는 중앙형으로 큰 바이너리와 잠금에 강하고, SVN은 간단한 대안입니다.
- 에디터는 리비전 컨트롤 플러그인으로 연결하고, 고치기 전에 체크아웃하고 끝나면 설명과 함께 제출하며, 배타적 잠금은 서버 정책으로 확인합니다.
- pak 기반 패치는 프로젝트 런처에서 기준 릴리스를 만들어 보관한 뒤,
Release Version This is Based On과Generate Patch로 만듭니다. - 패치의 변경 단위는 에셋 패키지라 값 하나만 바뀌어도 패키지 전체가 들어가며, 실행 코드의 변경은 콘텐츠 pak만으로 전달할 수 없습니다.
- 패치는 기존 설치본과 신규 설치를 나눠 시험하고, 소스 태그와 엔진 버전, 기준 릴리스, 패치 결과물을 함께 기록합니다.
연습 문제
1. 값 한 바이트만 바꿨다면
앞과 같은 조건에서 패키지 A(1 MiB)의 내부 값 한 바이트만 바꿨습니다. 이 콘텐츠 패치에 들어가는 크기는 얼마일까요? C++ 실행 코드도 함께 바꿨다면, A의 pak 패치만으로 코드의 변경까지 전달할 수 있을까요?
해설 보기
패치의 변경 단위는 에셋 패키지이므로, 한 바이트만 바꿔도 A 패키지 전체인 1 MiB가 패치에 들어갑니다.
실행 코드의 변경은 콘텐츠 pak만으로 전달되지 않으므로, 그 플랫폼의 실행 파일이나 패치 결과물도 함께 준비해야 합니다. 기존 설치본에 패치를 적용하는 확인과 새로 전체를 설치하는 확인을 나누고, 기준 릴리스와 플랫폼, 쿠킹 설정이 같은지 기록합니다. 이 숫자는 명시한 단순한 조건의 계산일 뿐, 실제 최종 다운로드 크기를 보장하지 않습니다.
2. 기준 릴리스를 보관하지 않았다면
1.0 버전을 출시할 때 프로젝트 런처의 릴리스 결과물을 보관하지 않았습니다. 이제 1.1 패치를 만들려고 합니다. 어떤 문제가 생길까요?
해설 보기
패치는 기준 릴리스의 에셋 레지스트리와 쿠킹한 결과를 기준으로 무엇이 바뀌었는지 비교해 만듭니다. 1.0의 릴리스 결과물이 없으면 무엇이 바뀌었는지 비교할 기준이 없어, 바뀐 것만 담은 패치를 만들 수 없습니다.
1.0을 만든 정확한 소스 태그와 엔진, 쿠킹 설정으로 기준 릴리스를 다시 만들 수 있다면 그것을 기준으로 삼을 수 있지만, 결과가 실제로 배포한 1.0과 같은지 확인해야 합니다. 그래서 출시할 때마다 기준 릴리스의 결과물과 그것을 만든 소스 태그, 엔진 버전을 함께 보관합니다.
3. 같은 재질을 두 사람이 고쳤다
Git LFS를 쓰는 팀에서 두 사람이 같은 재질 에셋을 각자 고쳐 커밋했고, 합치려 하자 충돌이 났습니다. 다음부터 이런 일을 막으려면 무엇을 해야 할까요?
해설 보기
재질 에셋은 바이너리라서 두 사람의 변경을 줄 단위로 합칠 수 없습니다. 그래서 둘 가운데 하나를 고르고, 다른 쪽의 작업은 다시 해야 합니다.
다음부터는 에셋을 가져오기 전에 잠금 정책을 정해, 한 사람이 바이너리 에셋을 고치는 동안 다른 사람이 고치지 못하게 합니다. Git LFS에서는 호스팅 서버가 지원하는 파일 잠금 기능을, Perforce라면 typemap의 배타적 체크아웃을 쓰고, 팀이 그 규칙을 지키도록 합니다.
패치를 만드는 과정은 Epic의 패치 만들기 안내에서 확인할 수 있습니다.
다음 글 사용자 피드백을 통한 환경 개선에서는 플레이어의 피드백을 모으고 분류해, 무엇을 먼저 고칠지 정하고 환경을 개선하는 방법을 다룹니다.