큰 이진 파일
고친 사람 github-actions[bot]
큰 이진 파일은 버전관리 저장소가 쓸데없이 무거워지는 문제입니다. 영상처럼 크고 글자가 아닌 파일을 코드와 함께 커밋하면 생깁니다. 파일을 고칠 때마다 한 벌이 이력에 더 쌓입니다. 나중에 지워도 줄지 않습니다. 두 사람이 같은 파일을 고치면 합칠 방법도 없습니다.
쉽고 빠른 이해
무슨 일인가 — 저장소가 계속 불어나는 일입니다. 50메가바이트짜리 영상을 처음 넣고 열 번 고쳐 커밋하면 이력에 영상 열한 벌이 쌓입니다.
왜 곤란한가 — 저장소를 처음 받는 사람은 그 열한 벌을 전부 내려받아야 합니다. 받기가 느려지고 디스크가 찹니다. 같은 파일을 둘이 고치면 한 사람의 작업은 버려야 합니다.
어떻게 터지나
- 글자가 아닌 큰 파일을 코드와 같은 저장소에 커밋합니다
- 고칠 때마다 바뀐 부분이 아니라 파일이 거의 한 벌씩 이력에 더해집니다
- 지워도 지나간 커밋들이 그 파일을 품고 있어서 저장소 크기는 그대로입니다
피하는 대가 — 큰 파일을 저장소 밖에 맡기면 저장소는 가벼워집니다. 대신 파일을 둘 서버를 따로 굴려야 합니다. 그 서버가 없으면 옛 판을 못 꺼냅니다.
상세
이 절은 크고 글자가 아닌 파일이 버전관리와 왜 안 맞는지를 다룹니다. 텍스트 파일과 견주어 보고, 파일 하나를 열 번 고쳐 커밋하는 장면으로 문제가 쌓이는 모습을 봅니다.
버전관리는 파일이 어떻게 바뀌어 왔는지 이력을 남기는 도구입니다. 이력이 있으면 어제 판으로 돌아가거나 누가 무엇을 바꿨는지 찾을 수 있습니다. 이 도구들은 대개 소스 코드처럼 글자로 된 파일을 다루려고 만들어졌습니다.
텍스트 파일과 이진 파일
텍스트 파일은 사람이 읽는 글자를 줄 단위로 담습니다. 소스 코드와 설정 파일이 그렇습니다. 버전관리 도구는 두 판을 줄마다 견줘 어느 줄이 바뀌었는지 보여 줍니다. 이 비교를 차이 비교라고 부릅니다.
이진 파일은 글자가 아니라 프로그램이 해석하는 바이트로 된 파일입니다. 이미지, 영상, 음원, 압축 파일, 디자인 도구의 원본 파일이 여기 듭니다. 이진 파일에는 줄이라는 단위가 없습니다. 그래서 줄마다 견주는 방식이 통하지 않습니다.
고칠 때마다 한 벌씩 쌓인다
텍스트 파일은 한 줄을 고치면 그 한 줄만 달라집니다. 그래서 버전관리 도구는 바뀐 부분만 적어 둘 수 있습니다. 나머지는 앞 판에서 빌려 씁니다. 이렇게 차이만 저장하는 방식을 델타 압축이라고 부릅니다. 판이 수백 개여도 저장소가 크게 늘지 않는 까닭입니다.
이진 파일은 사정이 다릅니다. 영상이나 이미지는 대개 이미 압축된 형식으로 저장됩니다. 여기서 압축은 파일 형식이 제 크기를 줄이려고 스스로 하는 일입니다. 앞에서 본 델타 압축처럼 판과 판 사이의 차이를 적는 일과는 다릅니다.
이렇게 압축된 파일은 화면 한 귀퉁이만 고쳐도 바이트가 넓게 뒤바뀌기 쉽습니다. 압축은 앞에 나온 내용을 바탕으로 뒤를 줄여 적는 경우가 많아서 앞쪽 한 곳이 달라지면 그 뒤가 줄줄이 달라집니다. 차이만 적으려 해도 차이가 파일 대부분이라서 소용이 없습니다.
결과로 이진 파일은 고칠 때마다 거의 한 벌씩 더해집니다. 50메가바이트 영상을 처음 넣고 열 번 고쳐 커밋하면 저장소에는 열한 벌, 550메가바이트 가까이 쌓입니다. 코드 몇 메가바이트짜리 저장소가 영상 하나 때문에 수백 배로 불어나는 일이 이렇게 생깁니다.
모두가 모든 판을 받는다
분산 버전관리는 저장소를 받는 사람마다 이력 전체를 복사해 가는 방식입니다. Git이 이 방식을 씁니다. 서버가 없어도 이력을 보고 커밋할 수 있게 하려고 이렇게 만들었습니다.
이 설계가 큰 이진 파일과 만나면 비용이 사람 수만큼 늘어납니다. 저장소를 받는 일을 클론이라고 합니다. 클론할 때마다 큰 파일의 모든 판이 따라옵니다.
지금 쓰는 판은 하나뿐입니다. 그런데도 지나간 판까지 전부 내려받습니다. 받는 사람의 디스크도 그만큼 찹니다.
사람만 받는 것이 아닙니다. 지속적 통합(CI, Continuous Integration)은 코드가 올라올 때마다 서버가 저장소를 받아 빌드하고 테스트하는 일입니다. 이 서버도 빌드마다 저장소를 받습니다. 저장소가 무거우면 빌드 하나하나가 받기부터 느려집니다.
지워도 줄지 않는다
큰 파일을 실수로 넣었다면 지우면 될 것 같습니다. 그런데 지우는 커밋을 하나 더 만들어도 저장소는 가벼워지지 않습니다. 지운 것은 지금 판에서뿐입니다. 지나간 커밋들은 그 파일을 여전히 품고 있습니다.
아래는 영상 하나를 커밋하고 열 번 고친 뒤 지운 경우입니다. 마지막 줄에서 새로 클론한 사람이 무엇을 받는지 보면 됩니다.
git add video.mp4 # 50메가 영상
git commit -m "첫 판"
# 열 번 고쳐 커밋한다
git rm video.mp4 # 지금 판에서만 지운다
git commit -m "삭제"
git clone <주소> # 열한 판을 다 받는다
마지막 커밋에는 영상이 없습니다. 그래도 앞의 열한 커밋이 각자 한 벌씩 품고 있습니다. 그래서 새로 클론한 사람은 열한 벌을 다 받습니다. 아래 그림이 위 코드가 남긴 이력입니다.
flowchart TD
subgraph 이력["저장소의 이력"]
C1["첫 커밋 · 영상 1판"] --> C2["열 번 고친 커밋 · 영상 2판~11판"]
C2 --> C3["마지막 커밋 · 영상을 지움"]
end
이력 --> K["새로 클론한 사람 · 1판부터 11판까지 다 받는다"]
이력에서까지 걷어내려면 그 파일이 들어간 커밋들을 모두 다시 만들어야 합니다. 커밋이 바뀌면 이미 저장소를 받아 간 사람들의 이력과 어긋납니다.
그래서 다시 만든 이력을 서버에 덮어써야 합니다. 다른 사람들은 저장소를 새로 받아야 합니다. 이 덮어쓰기는 이력 강제 덮어쓰기에서 다룹니다.
합칠 수 없는 파일
두 사람이 같은 파일을 따로 고친 뒤 합치는 일을 병합이라고 합니다. 텍스트 파일은 서로 다른 줄을 고쳤다면 도구가 알아서 합칩니다.
그런데 같은 줄을 서로 다르게 고쳤다면 도구는 어느 쪽을 남길지 모릅니다. 그래서 거기서 멈추고 사람에게 묻습니다. 이 멈춤이 병합 충돌입니다.
이진 파일은 줄이 없으니 파일 전체가 하나의 줄과 같습니다. 두 사람이 한 귀퉁이씩만 고쳤어도 도구는 파일 전체가 충돌했다고 봅니다. 사람이 고를 수 있는 것도 둘 중 어느 파일을 남길지뿐입니다. 고르지 않은 쪽의 작업은 손으로 다시 해야 합니다.
이 문제를 막는 방법으로 파일 잠금이 있습니다. 누군가 파일을 고치기 시작할 때 잠가 두면 다른 사람은 그동안 그 파일을 못 고칩니다. 충돌이 나지 않는 대신 고치는 사람이 한 번에 한 명으로 줄어듭니다.
터지는 조건
크기만으로 문제가 되지는 않습니다. 몇 메가바이트부터 크다고 정한 선은 없습니다. 아래 세 조건이 겹칠수록 아파집니다.
| 조건 | 무엇이 나빠지나 |
|---|---|
| 차이만 적기 어려운 이진 형식이다 | 고칠 때마다 한 벌씩 더해진다 |
| 자주 고친다 | 판 수만큼 저장소가 불어난다 |
| 여럿이 같은 파일을 고친다 | 합칠 수 없어 한쪽 작업을 버린다 |
한 번 넣고 거의 안 고치는 작은 아이콘은 세 조건에 다 안 걸립니다. 매일 고치는 수백 메가바이트짜리 게임 그래픽 원본은 셋 다 걸립니다.
피하는 방법
이 소절은 큰 이진 파일을 저장소에서 덜어내는 네 가지 방법을 견줍니다. 공통된 생각은 하나입니다. 이력을 남길 필요가 없는 파일은 넣지 않습니다. 남길 파일은 무게를 저장소 밖으로 옮깁니다.
| 방법 | 하는 일 | 대가 |
|---|---|---|
| 아예 커밋하지 않기 | 빌드하면 다시 나오는 파일은 커밋에서 빼고 결과물은 저장소 밖에 둔다 | 어느 커밋으로 만든 결과물인지 저장소만 보고는 알 수 없다 |
| 저장소 밖에 맡기기 | Git LFS(Large File Storage, 대용량 파일 저장소)처럼 저장소에는 짧은 가리킴 파일만 두고 실물은 다른 서버에 둔다 | 그 서버를 따로 굴려야 한다 |
| 일부만 받기 | 얕은 클론처럼 최근 이력만 받는다 | 옛 판을 보려면 다시 받아야 한다 |
| 다른 도구 쓰기 | 내 컴퓨터에는 최신본만 받아 두는 Subversion·Perforce 같은 중앙 집중형 도구를 쓴다 | 서버 없이는 이력을 못 본다 |
첫째 방법은 빌드하면 다시 나오는 파일을 아예 저장소에 넣지 않는 것입니다. 컴파일한 실행 파일처럼 소스 코드만 있으면 다시 만들 수 있는 파일이 여기 듭니다. 이런 파일 이름은 .gitignore에 적어 둡니다. Git은 이 파일에 적힌 이름을 커밋에서 뺍니다.
빌드 결과물을 판마다 남겨야 하면 아티팩트 저장소에 맡깁니다. 빌드 결과물을 모아 두는 서버입니다. 다만 결과물이 저장소 밖에 있으니 어느 커밋으로 만든 결과물인지는 저장소만 보고 알 수 없습니다.
둘째 방법은 가리킴 파일을 쓴다는 점이 핵심입니다. 가리킴 파일은 실물 파일이 어디 있는지만 적은 몇 줄짜리 글자 파일입니다. 이력에는 이 작은 파일만 쌓입니다. 실물은 지금 꺼낸 커밋이 가리키는 판만 내려받습니다. 그 서버가 멈추거나 사라지면 이력에는 가리킴 파일만 남아 옛 판을 못 꺼냅니다.
관련 항목
큰 이진 파일과 함께 터지는 버전관리 문제
병합 충돌 · 이력 강제 덮어쓰기 · 비밀값 커밋 · 분리된 HEAD · 십자 병합 · 장수 브랜치
큰 이진 파일을 저장소에서 덜어내는 수단
Git LFS · 아티팩트 저장소 · 객체 스토리지 · .gitignore · 얕은 클론 · 부분 클론 · 파일 잠금
큰 이진 파일이 불어나는 까닭이 되는 저장 방식
이진 파일 · 텍스트 파일 · 델타 압축 · 압축 · 차이 비교 · 리비전 이력 · 스냅샷
큰 이진 파일을 다루는 방식이 갈리는 버전관리 도구
Git · Subversion · Perforce · Mercurial · 분산 버전관리 · 중앙 집중형 버전관리
큰 이진 파일을 늘 쌓는 개발 분야
게임 개발 · 에셋 파이프라인 · 데이터셋 · 모델 가중치 · 데이터 버전관리
큰 이진 파일이 속하는 상위 분류
다른 이름: large binary file · 큰 바이너리 파일 · 대용량 바이너리 파일