본문 바로가기

에셋의 버전 관리

바이너리 에셋의 변경 이력과 충돌 특성을 이해하고 팀 규모에 따라 Perforce·Git·백업 방식을 선택합니다.

프로젝트가 진행되는 동안 에셋은 계속 고쳐지고 바뀝니다. 문제는 변경 자체가 아니라, 그 변경의 이력이 남지 않을 때 생깁니다. 어제는 잘 보이던 재질이 오늘 깨졌는데 누가 언제 무엇을 바꿨는지 모른다면, 원인을 찾을 수도 되돌릴 수도 없습니다. 여러 사람이 같은 에셋을 동시에 고치다 한쪽의 작업이 사라지는 일도 팀 개발에서는 흔합니다.

이 글에서는 언리얼 프로젝트에서 버전 관리가 왜 필요한지, 특히 에셋 파일이 코드와 어떻게 다른지부터 살펴봅니다. 그다음 대표적인 두 버전 관리 시스템인 Perforce와 Git의 특징과 작업 흐름을 비교하고, 혼자 작업할 때 쓸 수 있는 간단한 방법과 그 한계를 정리합니다.


버전 관리가 필요한 이유

버전 관리 시스템(Version Control System, VCS)은 파일의 변경 사항을 기록하고 관리하는 도구입니다. 언리얼 엔진 5에서는 에디터 메뉴에서 이 기능을 리비전 컨트롤(Revision Control)이라고 부르며, 예전 버전에서는 소스 컨트롤(Source Control)이라고 불렀습니다. 언리얼 프로젝트는 .uasset과 .umap 같은 에셋 파일이 매우 많기 때문에, 버전 관리 없이 프로젝트를 오래 유지하기는 어렵습니다.

버전 관리를 쓰면 먼저 변경 이력을 추적할 수 있습니다. 추적 대상으로 등록하고 커밋하거나 제출한 시점의 상태로 언제든 되돌릴 수 있으므로, 예기치 않은 오류나 데이터 손실이 생겨도 이전의 안정된 상태로 복구할 수 있습니다. 또 변경을 나눠 공유하고 서로의 변경이 충돌하는지 확인할 수 있어 협업이 쉬워집니다. 원격 저장소에 제출해 둔 이력은 내 컴퓨터가 고장 나도 다시 받아 올 수 있습니다. 다만 아직 제출하지 않은 작업과 외부 원본 파일, 그리고 저장소 자체의 백업은 따로 관리해야 합니다.

에셋 파일은 코드와 다르다

언리얼의 에셋 파일은 바이너리(binary) 파일입니다. 바이너리 파일은 텍스트처럼 사람이 줄 단위로 읽을 수 있는 형태가 아니라, 엔진이 읽는 형식으로 저장된 데이터입니다. 이 차이가 버전 관리 방식을 바꿉니다.

코드 파일은 두 사람이 같은 파일의 다른 줄을 고쳤다면 버전 관리 시스템이 두 변경을 자동으로 합칠(merge) 수 있습니다. 하지만 바이너리 에셋은 어느 부분이 바뀌었는지 줄 단위로 비교할 수 없어서 자동으로 합칠 수 없습니다. 두 사람이 같은 재질을 동시에 고쳤다면 결국 한쪽의 버전을 고르고 다른 쪽의 작업은 버려야 합니다. 그래서 에셋은 처음부터 누가 어떤 에셋을 편집할지 정하고, 한 사람이 편집하는 동안 다른 사람이 못 고치게 막는 잠금(lock) 규칙을 함께 운영해야 합니다.

그래서 버전 관리 방식은 팀의 규모보다 먼저, 다루는 파일이 자동으로 합칠 수 있는 파일인지 잠가야 하는 파일인지를 기준으로 판단하면 고르기 쉽습니다. 언리얼 프로젝트에서 주로 쓰는 버전 관리 시스템은 Perforce P4(예전 이름 Helix Core)와 Git입니다.


Perforce: 큰 바이너리와 잠금에 강한 중앙 방식

Perforce는 대규모 프로젝트와 팀에서 널리 쓰는 버전 관리 시스템입니다. 언리얼 에디터와 긴밀하게 통합되어 있고, 특히 큰 바이너리 파일을 관리하는 데 알맞습니다.

Perforce는 중앙 서버가 제출된 파일의 이력을 관리하는 중앙 집중식입니다. .uasset 같은 큰 바이너리 파일을 효율적으로 저장하고 동기화할 수 있으며, 파일 잠금 기능으로 한 파일을 한 번에 한 사람만 고치게 해 동시 편집 충돌을 줄입니다. 다만 잠금이 저절로 걸리는 것은 아닙니다. 서버의 파일 유형 설정(typemap)에서 .uasset과 .umap 같은 파일에 배타적 체크아웃(+l)을 적용하고, 팀이 그 규칙을 따른다는 것이 전제입니다. 또 언리얼 에디터 안에서 체크아웃과 제출 같은 Perforce 기능을 바로 쓸 수 있습니다.

Perforce의 작업 흐름은 다음과 같이 이어집니다. 먼저 Perforce 서버를 구축하고 프로젝트 저장소를 만드는데, 이 단계에는 서버를 구축하고 관리하는 별도의 지식이 필요합니다. 각 개발자는 Perforce 클라이언트인 P4V를 설치해 서버에 연결하고, 언리얼 에디터의 리비전 컨트롤 메뉴에서 제공자를 Perforce로 정해 로그인합니다.

실제 작업은 동기화에서 시작합니다. 작업하기 전에 서버의 최신 상태를 받아 오고, 고칠 파일을 체크아웃(check out)합니다. 이때 그 파일이 배타적 잠금이 걸리는 유형인지 확인합니다. 그다음 에디터에서 에셋을 고치고, 고친 에셋을 참조하는 다른 에셋과 레벨도 함께 저장한 뒤 변경 목록(changelist)을 확인합니다. 필요한 동기화와 충돌 해결을 마쳤다면 서버에 제출(submit)하며, 이때 바뀐 파일 목록과 함께 무엇을 왜 바꿨는지 설명하는 메시지를 남깁니다. 새 파일을 덮어쓰기 전에 변경 이유와 되돌릴 기준을 남기는 습관이 에셋 버전 관리의 출발점입니다.


Git: 코드에 강한 분산 방식

Git은 세계에서 가장 널리 쓰이는 분산형 버전 관리 시스템입니다. 소규모 팀이나 개인 프로젝트에서 특히 많이 쓰며, GitHub, GitLab, Bitbucket 같은 호스팅 서비스로 쉽게 시작할 수 있습니다.

분산형이라는 말은 개발자마다 자기 컴퓨터의 로컬 저장소에 이력을 통째로 가지고 있다는 뜻입니다. 그래서 네트워크 없이도 로컬에서 커밋할 수 있지만, 아직 내려받지 않은 원격 변경이나 Git LFS 객체를 받으려면 연결이 필요합니다. Git은 텍스트로 된 코드 파일의 변경을 관리하는 데 특히 강하고, 브랜치(branch)로 메인 브랜치에 영향을 주지 않는 별도의 변경 이력을 만들어 기능 개발이나 버그 수정을 따로 진행할 수 있습니다.

바이너리 에셋을 Git에 그대로 넣으면 저장소가 금방 커지므로, 큰 바이너리 파일에는 Git LFS(Large File Storage)를 씁니다. Git LFS는 파일 내용을 별도 객체로 저장하고 저장소에는 그 파일을 가리키는 작은 정보만 남깁니다. 다만 LFS를 쓴다고 바이너리가 자동으로 합쳐지는 것은 아니고, 잠금도 호스팅 서버의 지원과 팀의 사용 규칙이 있어야 동작합니다.

Git의 작업 흐름은 다음과 같습니다. Git을 설치하고 프로젝트 폴더에서 저장소를 초기화한 뒤, Git LFS를 설정해 git lfs track "*.uasset"처럼 바이너리 확장자를 추적하게 합니다. .umap처럼 팀에서 쓰는 다른 바이너리 유형도 함께 추적하고, 이 설정이 기록된 .gitattributes 파일을 커밋합니다. 또 Binaries, Intermediate, Saved, DerivedDataCache처럼 엔진이 다시 만들 수 있는 폴더는 .gitignore로 저장소에서 뺍니다. 편집 도구는 에디터의 Git 제공자나 플러그인 지원을 확인해 연결하거나, 외부 Git 클라이언트를 씁니다.

작업할 때는 먼저 원격 변경을 가져와 로컬과 합치고 잠금 규칙을 확인합니다. 에디터에서 에셋을 고친 뒤 바뀐 파일을 스테이징(staging)하고 커밋해 로컬 저장소에 기록하고, 푸시(push)해 원격 저장소에 공유합니다. 다른 팀원의 변경은 수시로 가져와 합치는데, 그 전에 커밋하지 않은 로컬 변경이 있는지 확인합니다. 바이너리 에셋이 충돌했다면 어느 쪽 버전을 쓸지 담당자와 정해야 합니다.


저장, 커밋, 푸시는 서로 다른 단계

Git에서 특히 헷갈리기 쉬운 것이 저장과 커밋, 푸시의 차이입니다. 문 재질을 파란색으로 바꾸고 로컬 Git 커밋까지 마쳤지만 아직 푸시하지 않았다고 해 봅시다. 내 저장소에는 되돌릴 수 있는 이력이 생겼지만, 다른 팀원이 원격에서 받는 버전에는 아직 그 변경이 없습니다.

정리하면 에디터의 저장은 에셋 파일을 지금 상태로 갱신하는 일이고, 커밋은 그 상태를 로컬 이력으로 기록하는 일이며, 푸시는 그 이력을 원격에 올려 공유하는 일입니다. 셋 가운데 하나만 했다고 다른 단계까지 된 것은 아닙니다. 그리고 제출하기 전에는 고친 메시만이 아니라, 바뀐 재질과 그것을 쓰는 레벨처럼 관련된 파일이 모두 목록에 들어 있는지 확인합니다. 메시만 올리고 재질을 빠뜨리면, 다른 사람의 화면에서는 새 메시에 옛 재질이 붙어 보입니다.


혼자 작업할 때의 간단한 방법

혼자 하는 작은 프로젝트라면 버전 관리 서버를 갖추는 것이 부담스러울 수 있습니다. 이럴 때는 다음과 같은 간단한 방법을 쓸 수 있습니다.

가장 단순한 방법은 중요한 변경이 있을 때마다 프로젝트 폴더 전체를 압축해, MyProject_20250624_v1.0.zip처럼 날짜나 버전을 붙여 백업하는 것입니다. 에디터의 자동 저장도 함께 씁니다. 에디터 환경설정의 Loading & Saving에서 자동 저장의 간격과 대상을 확인할 수 있는데, 자동 저장은 갑작스러운 종료에 대비한 복구용 사본이라 프로젝트 전체의 이력이나 다른 장치에 둔 백업을 대신하지는 못합니다.

중요한 레벨을 크게 고치기 전에는 File 메뉴의 Save Current Level As로 MyLevel_v1.umap, MyLevel_v2_Test.umap처럼 다른 이름의 레벨을 저장해 두는 습관도 도움이 됩니다. 하지만 이 방법에는 분명한 한계가 있습니다. 레벨을 다른 이름으로 저장하면 레벨 파일만 복제될 뿐, 그 레벨이 참조하는 재질이나 블루프린트는 복제되지 않습니다. 그래서 두 레벨이 같은 재질을 쓰고 있다면, 그 재질을 바꿨을 때 두 레벨 모두에 변경이 보입니다. 즉 이 방법은 프로젝트 전체의 스냅샷이 아닙니다.

이런 방법은 전문적인 버전 관리 시스템에 비하면 부족하지만, 간단한 개인 프로젝트에서는 쓸모가 있습니다. 그러나 프로젝트가 커지거나 다른 사람과 함께 작업하기 시작한다면 Perforce나 Git 같은 버전 관리 시스템을 도입하는 것이 좋습니다.


대상별로 관리할 기준

마지막으로 코드와 에셋, 백업을 각각 어떤 기준으로 관리하는지 묶어 보면 다음과 같습니다.

대상관리할 기준
코드텍스트로 비교할 수 있으므로, 변경 내용의 차이(diff)를 읽고 테스트로 검토합니다.
에셋자동으로 합칠 수 없으므로, 편집 담당과 잠금을 정해 충돌을 피하고, 관련 레벨까지 함께 확인합니다.
백업원본 파일과 엔진 버전, 변경 설명을 함께 남기고, 실제로 복원할 수 있는지 확인합니다.

백업은 만들어 두는 것만으로 끝나지 않습니다. 압축 파일이 깨져 있거나 다른 엔진 버전에서만 열리는 상태라면 쓸 수 없으므로, 가끔은 백업을 실제로 풀어 프로젝트가 열리는지 확인합니다.


더 깊이: 레벨을 액터별 파일로 나누는 One File Per Actor

레벨도 하나의 .umap 바이너리 파일이라, 예전 방식에서는 레벨에 놓인 액터 하나를 고치려 해도 레벨 파일 전체를 체크아웃해야 했고 그동안 다른 팀원은 그 레벨을 고칠 수 없었습니다. One File Per Actor 문서에 따르면 액터당 파일 하나(One File Per Actor, OFPA)는 레벨에 배치한 액터의 데이터를 외부 파일에 따로 저장해, 액터를 고칠 때 레벨 파일 자체를 저장하지 않아도 되게 합니다. 그래서 두 사람이 같은 레벨에서 서로 다른 액터를 고치면 서로 다른 파일을 체크아웃하게 됩니다. 이 기능은 월드 파티션(World Partition)을 쓰면 기본으로 켜지고, 월드 파티션을 쓰지 않는 레벨에서는 World Settings의 Use External Actors로 켭니다.

OFPA에는 알아 둘 제약도 있습니다. 이 기능은 에디터에서만 쓰이며, 쿠킹할 때는 모든 액터가 다시 각자의 레벨 파일 안에 합쳐집니다. 또 외부 액터 파일의 이름은 인코딩되어 있어서 소스 관리 프로그램에서는 어느 액터의 파일인지 알아보기 어렵습니다. 여러 파일을 체크아웃해 두고 일부만 제출하면 끊어진 참조가 남을 수 있으므로, 문서는 OFPA를 쓸 때 콘텐츠와 액터 파일을 에디터 안에서 제출하고 View Changelist 창으로 변경 목록의 내용을 확인하라고 안내합니다. 다만 이 변경 목록 기능은 Perforce를 쓸 때만 지원됩니다.


정리

  • 버전 관리는 변경 이력을 남겨 되돌리고 협업하게 해 주며, UE5 에디터에서는 이 기능을 리비전 컨트롤이라고 부릅니다.
  • 에셋은 바이너리 파일이라 자동으로 합칠 수 없으므로, 편집 담당과 잠금 규칙이 필요합니다.
  • Perforce는 중앙 집중식으로 큰 바이너리와 배타적 잠금에 강하며, 잠금은 typemap의 +l 설정과 팀 규칙이 있어야 동작합니다.
  • Git은 분산형으로 코드에 강하며, 바이너리는 Git LFS로 관리하고 .gitattributes를 커밋하며, 다시 만들 수 있는 폴더는 .gitignore로 뺍니다.
  • 저장은 파일 갱신, 커밋은 로컬 이력 기록, 푸시는 원격 공유로 서로 다른 단계이며, 제출 전에 관련 파일이 모두 들어갔는지 확인합니다.
  • 폴더 백업, 자동 저장, 다른 이름으로 레벨 저장은 개인 프로젝트에서 쓸 수 있지만, 다른 이름으로 저장한 레벨은 참조한 에셋까지 복제하지 않습니다.

연습 문제

1. 커밋했는데 팀원에게 안 보인다

문 재질을 파란색으로 바꾸고 저장한 뒤 Git으로 커밋했습니다. 팀원이 원격 저장소에서 최신 상태를 받았는데 문이 여전히 예전 색입니다. 무엇이 빠졌을까요?

해설 보기

푸시가 빠졌을 가능성이 큽니다. 커밋은 내 로컬 저장소에 이력을 기록할 뿐이고, 원격 저장소에 올리려면 푸시를 해야 합니다.

푸시했는데도 안 보인다면 커밋 목록에 재질 에셋이 실제로 들어갔는지 확인합니다. 메시나 레벨만 커밋하고 재질 파일을 빠뜨렸다면, 팀원은 새 레벨에 옛 재질을 받게 됩니다.

2. 다른 이름으로 저장한 레벨

MyLevel.umap을 MyLevel_v2_Test.umap으로 다른 이름으로 저장한 뒤, 두 레벨이 함께 쓰는 벽 재질의 색을 바꿨습니다. 예전의 MyLevel.umap을 열면 벽은 어떤 색일까요? 바꾸기 전의 전체 상태로 돌아가려면 무엇이 필요할까요?

해설 보기

바뀐 색으로 보입니다. 다른 이름으로 저장하면 레벨 파일만 복제되고, 레벨이 참조하는 재질 에셋은 하나 그대로이기 때문입니다. 두 레벨의 벽은 같은 재질을 가리키므로 재질을 바꾸면 둘 다 바뀝니다.

바꾸기 전의 전체 상태로 돌아가려면 레벨만이 아니라 관련 에셋의 이전 상태도 함께 있어야 합니다. 그래서 버전 관리 시스템에서 레벨과 재질의 이력을 함께 되돌리거나, 프로젝트 전체를 백업해 둔 사본을 써야 합니다.

3. 같은 재질을 두 사람이 고쳤다

잠금 규칙 없이 Git으로 작업하던 두 사람이 같은 M_Door 재질을 각자 고쳐 커밋했습니다. 한 사람이 다른 사람의 변경을 가져오려 하자 충돌이 났습니다. 코드 파일처럼 두 변경을 합칠 수 있을까요? 다음부터는 어떻게 막을 수 있을까요?

해설 보기

합칠 수 없습니다. 재질 에셋은 바이너리 파일이라 어느 부분이 바뀌었는지 줄 단위로 비교해 합칠 수 없으므로, 두 버전 가운데 하나를 고르고 다른 쪽의 작업은 다시 해야 합니다. 어느 버전을 쓸지는 두 사람과 담당자가 정합니다.

다음부터는 에셋마다 편집 담당을 정하고, Git LFS의 잠금 기능이나 Perforce의 배타적 체크아웃처럼 한 사람이 편집하는 동안 다른 사람이 못 고치게 하는 규칙을 운영합니다.

에디터와 버전 관리 시스템의 연결은 Epic의 소스 컨트롤 안내에서 확인할 수 있습니다.

다음 글 에셋 최적화 및 압축에서는 텍스처와 메시, 사운드 에셋의 용량과 성능을 줄이는 설정을 살펴봅니다.