Git LFS vs. 일반 Git: 대규모 에셋에 대한 성능 영향
분산 버전 관리 시스템의 기반인 Git은 텍스트 기반 소스 코드의 변경 사항을 추적하는 데 탁월합니다. Git의 효율성은 콘텐츠 주소 저장에 크게 의존하며, 이는 델타 압축을 사용하여 히스토리 전반에 걸쳐 작고 점진적인 변경 사항을 관리합니다. 그러나 이 모델은 멀티미디어 에셋, 게임 텍스처 또는 대규모 데이터 세트와 같은 대형 바이너리 파일에 적용될 때 상당한 성능 문제를 야기합니다.
비텍스트 데이터에 크게 의존하는 프로젝트의 경우 일반 Git을 사용하면 저장소 비대, 느린 복제 시간 및 리소스 비효율성으로 빠르게 이어질 수 있습니다. 이 글은 일반 Git과 Git Large File Storage (LFS) 간의 포괄적인 성능 비교를 제공하며, 각 시스템의 메커니즘을 자세히 설명하고 LFS가 대규모 에셋을 효율적으로 관리하는 데 필요한 최적화 도구가 될 때를 식별합니다.
일반 Git의 성능 병목 현상
Git LFS가 존재하는 이유를 이해하려면 먼저 일반 Git이 파일을 처리하는 방식, 특히 이 접근 방식이 대형 바이너리 파일에 실패하는 이유를 살펴봐야 합니다.
콘텐츠 주소 저장 및 히스토리
Git의 핵심 설계 원칙은 커밋된 모든 파일의 모든 버전을 저장소 히스토리(.git 디렉토리) 내에 저장하도록 규정합니다. 저장소를 복제할 때 모든 히스토리 데이터, 즉 대형 바이너리 파일의 모든 버전이 로컬 머신으로 전송됩니다.
이 접근 방식은 두 가지 주요 이유로 바이너리 파일에 대해 잘 작동하지 않습니다.
- 비효율적인 델타 압축: 바이너리 파일(JPEG, MP4 또는 컴파일된 실행 파일과 같은)은 종종 이미 압축되어 있습니다. 이러한 파일에 작은 변경 사항만 적용되면 Git은 의미 있는 델타를 생성하는 데 어려움을 겪으며, 각 리비전에 대해 파일의 거의 전체 복사본이 히스토리에 저장되는 경우가 많습니다. 이는 저장소 크기 증가를 빠르게 가속화합니다.
- 필수 히스토리 전송: 저장소를 복제하려면 전체 히스토리를 다운로드해야 합니다. 프로젝트에 50번 수정된 100MB 텍스처 파일이 포함되어 있다면, 초기 복제 시 해당 단일 에셋의 히스토리만으로도 수 기가바이트를 전송해야 합니다. 이는 특히 신규 기여자 또는 CI/CD 시스템의 경우 개발 속도에 심각한 영향을 미칩니다.
결과: 저장소가 방대해지고 복제 시간이 길어지며 백그라운드 유지 관리 작업(예: 가비지 컬렉션)이 느려지고 과도한 로컬 디스크 공간이 필요합니다.
Git Large File Storage (LFS) 소개
Git LFS는 GitHub(현재 널리 채택됨)에서 개발한 오픈 소스 확장 프로그램으로, Git이 지정된 파일 유형을 처리하는 방식을 변경합니다. LFS는 핵심 Git 저장소에서 저장 부담을 분산시켜 소스 코드의 Git 효율성을 유지하면서 대형 바이너리를 외부화합니다.
포인터 시스템
파일이 LFS로 추적될 때 실제 바이너리 콘텐츠는 Git 객체 데이터베이스에 저장되지 않습니다. 대신 LFS는 Git 저장소 내에 작고 표준화된 텍스트 포인터 파일을 저장합니다. 이 포인터는 실제 바이너리 콘텐츠의 위치를 참조하며, 이 콘텐츠는 LFS 서버(일반적으로 Git 원격 저장소(예: GitHub, GitLab, Bitbucket)와 함께 호스팅됨)에 저장됩니다.
LFS 포인터 파일은 다음과 유사하게 보입니다.
version https://git-lfs.github.com/spec/v1
oid sha256:4c2d44962ff3c43734e56598c199589d8995a643...a89c89
size 104857600
성능 이점: 필요할 때만 검색
LFS의 근본적인 성능 이점은 복제 또는 가져오기와 같은 작업 중에 Git이 작은 텍스트 포인터만 검색한다는 것입니다. 실제 대형 바이너리 파일은 일반적으로 체크아웃 작업(git checkout 또는 git lfs pull) 중에 필요한 경우에만 다운로드됩니다.
성능 비교: LFS vs. 일반 Git
다음 표는 대규모 에셋을 관리할 때 중요한 개발 작업 전반에 걸친 성능 차이를 요약합니다.
| 작업 | 일반 Git 성능 | Git LFS 성능 | 이점 | 근거 |
|---|---|---|---|---|
| 초기 복제 | 낮음/매우 느림 | 우수/빠름 | LFS | 작은 포인터만 다운로드; 필요에 따라 바이너리 검색. |
| 저장소 크기 | 매우 큼(비대) | 작음(얇음) | LFS | 바이너리가 .git 디렉토리 외부로 이동됨. |
| 체크아웃/전환 | 느림/높은 I/O | 빠름 | LFS | HTTP를 통해 필요한 특정 바이너리 버전만 검색. |
| CI/CD 빌드 시간 | 느림(대규모 복제 때문) | 빠름 | LFS | 복제 및 종속성 가져오기 시간 대폭 감소. |
| 히스토리 검토 | 전체 히스토리 다운로드 필요 | 포인터만(빠름) | LFS | 히스토리가 간결하고 관리 가능하게 유지됨. |
1. 저장소 비대 및 유지 관리
일반 Git 저장소는 대규모 에셋이 커밋된 후에는 해당 에셋이 나중에 삭제되더라도 정리하기가 매우 어렵습니다(히스토리에 남아 있음). 이로 인해 복잡한 도구(git filter-branch 또는 git filter-repo)를 사용하여 히스토리를 영구적으로 다시 작성해야 하는데, 이는 파괴적이고 시간이 많이 소요되는 프로세스입니다.
LFS 영향: LFS는 대규모 파일을 외부화하므로 핵심 Git 저장소 크기는 일관되게 작고 관리하기 쉬우며, 가비지 컬렉션(git gc)과 같은 내부 Git 프로세스에 필요한 시간을 대폭 줄여줍니다.
2. 대역폭 및 네트워크 지연 시간
분산 팀의 경우 네트워크 대역폭이 주요 관심사입니다.
- 일반 Git: 모든 사용자가 전체 저장소 히스토리를 가져와야 하므로, 실제로 필요한 파일에 관계없이 모든 새 복제에 막대한 양의 대역폭이 소모됩니다.
- Git LFS: LFS는 현재 체크아웃된 커밋과 관련된 특정 바이너리 블롭만 전송합니다. 사용자가 최신 릴리스 브랜치만 작업하는 경우 해당 특정 버전에 필요한 바이너리만 다운로드하므로 대역폭을 크게 절약하고 프로세스를 가속화하며, 특히 느린 연결에서 더욱 그렇습니다.
3. 서버 부하
대규모 저장소를 관리하면 Git 서버에 높은 부하가 발생하며, 특히 대량의 데이터를 가져오거나 푸시하는 것과 같은 심층 작업 중에 더욱 그렇습니다. 대규모 파일 저장 메커니즘을 별도의 최적화된 LFS 서버(일반적으로 간단한 HTTP 또는 S3와 유사한 객체 저장 프로토콜 사용)로 전환하면, 핵심 Git 서버는 일반 소스 코드 작업에 대해 성능을 유지합니다.
Git LFS 사용 시기
Git LFS는 다음 기준을 모두 충족하는 모든 파일에 대한 최적의 선택입니다.
- 대규모: 일반적으로 500KB ~ 1MB 이상의 파일.
- 바이너리 형식: 압축이 잘 되지 않는 파일(예: 압축된 이미지, 비디오, 오디오).
- 빈번한 변경: 히스토리에 반복적인 버전을 생성하며 자주 업데이트되는 파일(예: 개발 중인 게임 에셋).
LFS 추적의 일반적인 후보:
*.psd,*.tiff,*.blend,*.max(디자인/3D 에셋)*.mp4,*.mov,*.wav(미디어 파일)*.dll,*.exe,*.jar(커밋되는 경우 컴파일된 바이너리)- 대규모
*.csv,*.parquet또는 데이터베이스 스냅샷 (데이터 과학)
Git LFS 구현
LFS 구현은 간단하며 LFS 클라이언트를 설치하고 LFS로 추적할 파일 패턴을 지정해야 합니다.
1단계: LFS 설치 및 초기화
먼저 컴퓨터에 Git LFS 클라이언트가 설치되어 있는지 확인합니다. 그런 다음 저장소 내부에서 설치 명령을 한 번 실행합니다.
git lfs install
2단계: 파일 형식 추적
LFS를 사용하여 관리할 파일 패턴을 Git에 알리려면 git lfs track을 사용합니다. 이 명령은 .gitattributes 파일을 생성하거나 업데이트하며, 이는 LFS가 올바르게 작동하는 데 중요합니다.
예: 모든 Photoshop 파일 및 대규모 비디오 파일 추적
git lfs track "*.psd"
git lfs track "assets/*.mp4"
# .gitattributes에 대한 변경 사항 검토
cat .gitattributes
# 예시 출력:
# *.psd filter=lfs diff=lfs merge=lfs -text
# assets/*.mp4 filter=lfs diff=lfs merge=lfs -text
3단계: 커밋 및 푸시
중요한 것은 추적된 파일과 함께 .gitattributes 파일을 커밋해야 한다는 것입니다. 푸시할 때 Git은 포인터를 전송하고 LFS 클라이언트는 대규모 바이너리를 LFS 저장소에 업로드합니다.
git add .gitattributes assets/
git commit -m "LFS 추적 PSD 및 MP4 추가"
git push
⚠️ 모범 사례:
.gitattributes먼저 커밋
.gitattributes파일은 추적하는 대규모 파일 이전 또는 동시에 커밋해야 합니다. 대규모 파일을 먼저 커밋하면 Git이 네이티브로 추적하여 LFS의 목적을 무효화합니다.
결론
일반 Git은 의도된 목적, 즉 소스 코드와 작은 구성 파일의 버전 관리에 있어서는 타의 추종을 불허합니다. 그러나 대규모 바이너리 에셋이 도입되면 저장소 비대 및 필수적인 히스토리 전송으로 인해 성능이 빠르게 저하됩니다.
Git LFS는 대규모 파일의 저장을 추상화하여 핵심 Git 저장소를 가볍고 빠르게 복제하며 유지 관리하기 쉽게 유지함으로써 중요한 성능 최적화를 제공합니다. 포인터 시스템과 필요할 때만 가져오기를 활용하여 LFS는 이전에 느렸던 작업을 빠른 프로세스로 전환하며, 게임 개발, 데이터 과학 및 상당한 규모의 자주 업데이트되는 바이너리 에셋을 다루는 모든 프로젝트에 필수적인 도구입니다.