Git에 대용량 파일을 커밋했을 때 — 히스토리 정리와 LFS 이전
동영상이나 디자인 원본을 커밋했다가 push가 거부되는 경우가 있습니다. GitHub는 100MB를 넘는 파일의 push를 차단하기 때문입니다. 더 조용히 진행되는 사고도 있습니다. 파일 하나하나는 제한에 걸리지 않지만 바이너리가 버전을 거듭해 쌓이면서, 어느 날 저장소가 수 GB로 불어나 clone에 십 분씩 걸리는 상황입니다. 두 경우 모두 원인은 같습니다. 한 번 커밋된 파일은 지워도 히스토리 속에 모든 버전이 남는다는 Git의 저장 모델입니다. 이 글은 문제 파일을 찾는 진단부터 히스토리 정리, 그리고 계속 필요한 파일이라면 LFS로 옮기는 절차까지 정리합니다.
진단 — 저장소에서 가장 큰 파일 찾기 #
먼저 저장소가 실제로 얼마나 무거운지 확인합니다.
git count-objects -vH
# count: 1324
# size-pack: 2.31 GiB
# ...size-pack이 히스토리 전체가 차지하는 실제 크기입니다. 작업 디렉터리에 보이는 파일 크기와 무관하게 이 값이 크다면 히스토리 어딘가에 무거운 blob이 있습니다. 범인은 파이프라인 하나로 찾을 수 있습니다.
git rev-list --objects --all |
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
awk '/^blob/ {print $3, $4}' |
sort -rn |
head -10
# 734003200 assets/demo-video.mp4
# 209715200 design/main.psd
# ...전체 히스토리의 모든 객체를 크기순으로 정렬한 결과입니다. 여기 나온 경로가 정리 대상 목록이 됩니다. 지금은 삭제된 파일이라도 히스토리에 남아 있으면 이 목록에 나타납니다.
방금 커밋했다면 — push 전 로컬 정리 #
아직 push하지 않았다면 문제는 내 컴퓨터 안에 있고, 정리도 간단합니다. 마지막 커밋에 들어갔다면 추적만 해제하고 커밋을 교체합니다.
git rm --cached assets/demo-video.mp4 # 추적 해제, 파일은 남음
echo "*.mp4" >> .gitignore
git add .gitignore
git commit --amend --no-edit몇 커밋 전에 들어갔다면 interactive rebase로 해당 커밋에서 멈춰 같은 작업을 합니다.
git rebase -i <해당 커밋의 직전 커밋>
# todo 리스트에서 해당 커밋을 edit 로 변경
git rm --cached assets/demo-video.mp4
git commit --amend --no-edit
git rebase --continuepush 전에 잡았다면 여기서 끝입니다. 아래의 협조 절차는 전혀 필요하지 않으므로, push 직전에 git diff --staged --stat으로 대용량 파일이 섞였는지 확인하는 습관의 값어치가 큽니다.
히스토리 깊숙이 있다면 — filter-repo #
이미 push됐고 여러 커밋에 걸쳐 쌓였다면 히스토리 전체를 다시 쓰는 작업이 필요합니다. 도구는 git filter-repo를 씁니다. filter-repo는 새로 clone한 저장소에서 실행하는 것을 전제로 하므로 정리 전용 클론부터 받습니다.
git clone https://github.com/example/my-service.git cleanup
cd cleanup
git filter-repo --strip-blobs-bigger-than 50M--strip-blobs-bigger-than은 지정 크기를 넘는 모든 blob을 전체 히스토리에서 제거합니다. 특정 파일이나 디렉터리만 제거하려면 경로 기준을 씁니다.
git filter-repo --invert-paths --path assets/demo-video.mp4
git filter-repo --invert-paths --path design/raw/정리된 히스토리는 모든 커밋 해시가 바뀐 새 히스토리이므로, 원격을 다시 연결해 강제로 올리고 팀 전원에게 re-clone을 공지해야 합니다. 옛 클론에서 push하면 지운 blob이 되살아납니다. 이 협조 절차는 비밀키 사고와 완전히 동일하며, 절차의 상세와 주의점은 Git에 비밀키를 커밋했을 때 — 키 회전과 filter-repo 히스토리 정리에서 다뤘습니다.
git remote add origin https://github.com/example/my-service.git
git push --force --all
git push --force --tags정리 후 — 계속 필요한 파일이라면 LFS로 #
제거로 끝나는 파일이 있고, 앞으로도 버전 관리가 필요한 파일이 있습니다. 디자인 원본이나 학습된 모델처럼 계속 다뤄야 하는 대용량 자산이라면 Git LFS로 옮깁니다. LFS는 저장소에 작은 포인터 파일만 남기고 실제 내용은 별도 스토리지에 저장하는 확장으로, 개요는 Git 실무 워크플로우 #7 모노레포와 Git — sparse-checkout·서브모듈·LFS에서 다뤘습니다.
이미 히스토리에 들어간 파일을 LFS로 바꾸는 전용 명령이 있습니다.
git lfs migrate import --include="*.psd" --everythingmigrate import는 지정 패턴의 파일을 전체 히스토리(--everything은 모든 브랜치 대상)에서 LFS 포인터로 바꿔 줍니다. 이것 역시 히스토리 재작성이므로 커밋 해시가 바뀌고, force push와 re-clone 공지라는 같은 협조 절차가 따라온다는 점은 동일합니다. 어차피 filter-repo로 히스토리를 다시 쓰는 참이라면, LFS 이전까지 한 번의 재작성 사이클로 묶어 처리하는 것이 팀을 두 번 번거롭게 하지 않는 방법입니다.
이전 후에는 새 파일이 자연스럽게 LFS로 들어가도록 추적 규칙을 커밋해 둡니다.
git lfs track "*.psd"
git add .gitattributes
git commit -m "Track PSD files with LFS"예방 #
- 기준을 알아 둡니다. GitHub는 50MB부터 경고를 출력하고 100MB부터 push를 차단합니다. 경고가 보였다면 이미 조치할 시점입니다.
- 처음부터 LFS로 시작합니다. 대용량 자산이 예정된 프로젝트라면 첫 커밋 전에
git lfs track규칙부터 잡아 둡니다. 히스토리에 들어간 뒤 옮기는 비용과 비교가 되지 않습니다. - 생성물은 .gitignore로 막습니다. 빌드 산출물, 압축 아카이브, 렌더링 결과물처럼 다시 만들 수 있는 파일은 애초에 버전 관리 대상이 아닙니다.
마무리 #
대응 순서를 정리합니다.
- 진단 —
git count-objects -vH와 blob 크기 파이프라인으로 범인 파일을 특정합니다. - 정리 — push 전이면 amend나 rebase로, push 후면 fresh clone에서 filter-repo로 히스토리를 다시 씁니다.
- 공지 — force push 후 팀 전원이 re-clone하게 안내합니다.
- 재배치 — 계속 필요한 자산은
git lfs migrate import로 LFS에 옮기고, 추적 규칙을 커밋합니다.
대용량 파일 사고는 비밀키 사고와 달리 시간을 다투지는 않습니다. 대신 방치할수록 히스토리에 버전이 쌓여 정리 범위가 커지므로, 50MB 경고가 보인 시점에 바로 처리하는 것이 가장 저렴한 대응입니다.