[TIPS] Git 대용량 파일(100MB 초과) 커밋 푸시 거부(GH001) 완벽 해결: 레거시 git filter-branch의 위험성 및 공식 권장 git-filter-repo로 저장소 다이어트 가이드
핵심 요약: GitHub에 코드를 푸시할 때 발생하는 remote: error: GH001: Large files detected 오류는 저장소에 100MB를 초과하는 대용량 파일(AI 모델 가중치, DB 덤프, 빌드 아티팩트, 영상 등)이 커밋되었을 때 원격 레포지토리가 수신을 거부하는 보안 정책입니다. 많은 개발자들이 git rm으로 파일을 삭제하고 새 커밋을 올리지만, Git의 스냅샷 객체 모델(DAG) 특성상 이전 커밋의 Blob 객체가 히스토리에 그대로 보존되어 푸시 거부가 계속됩니다. 또한 널리 공유되는 과거의 git filter-branch 명령은 공식적으로 사용이 중단(Deprecated)되었으며 속도가 극도로 느리고 저장소를 오염시킬 위험이 큽니다. 본 가이드에서는 Git의 내부 스토리지 메커니즘을 규명하고, Git 공식 권장 표준 도구인 git-filter-repo를 활용하여 저장소 히스토리에서 대용량 파일을 영구 삭제하고 용량을 수 기가바이트에서 수십 메가바이트로 다이어트하는 실무 프로덕션 절차, 도구별 성능 매트릭스, 자주 겪는 4대 치명적 에러 트러블슈팅, 그리고 Git LFS 전환 가이드와 5단계 복구 체크리스트를 완벽히 정리합니다.
1. 왜 단순 git rm과 구형 git filter-branch는 실패하는가?
로컬 환경에서 개발을 진행하다가 데이터베이스 백업 덤프(.sql), 딥러닝 모델 가중치(.pth, .onnx), 압축 아카이브(.zip), 동영상 파일(.mp4) 등을 무심코 커밋하고 git push를 실행하면 다음과 같은 치명적인 에러를 마주하게 됩니다.
이 오류를 보고 당황한 개발자들은 흔히 아래의 두 가지 치명적인 악수를 두게 됩니다.
1) 단순 git rm 및 git commit의 함정: Git 스냅샷과 Blob 객체의 메커니즘
많은 개발자가 즉시 git rm data/weights/model_large.bin을 수행하고 git commit -m "fix: remove large file"로 새 커밋을 생성한 뒤 다시 푸시를 시도합니다. 그러나 푸시는 여전히 똑같은 GH001 에러를 뿜으며 거부당합니다.
그 이유는 Git의 내부 저장소 구조가 변경 차분(Diff)이 아닌 스냅샷(Snapshot) 기반의 방향성 비순환 그래프(DAG, Directed Acyclic Graph)이기 때문입니다.
- 사용자가
git add를 실행하는 순간 142MB 크기의 바이너리 데이터는.git/objects/디렉토리 내에 고유한 SHA-1/SHA-256 해시를 가진Blob객체로 영구 기록됩니다. Commit 3에서 파일을 삭제했다는 것은 최신 Tree 객체에서 해당 파일명 매핑을 지웠다는 의미일 뿐,Commit 2의 부모 커밋과 트리, 그리고 그 트리가 가리키는 142MB Blob 객체는 Git 히스토리에 온전히 남아 있습니다.- GitHub에
git push를 실행하면 Git은 원격 서버와 로컬의 커밋 차이를 계산하고, 도달 가능한(Reachable) 모든 객체를 묶은 팩파일(Packfile)을 전송합니다. 원격 후크(pre-receive hook)는 푸시되는 팩파일 전체를 검사하므로, 이전 커밋에 100MB 초과 파일이 포함되어 있는 한 절대로 푸시를 허용하지 않습니다.
2) 레거시 git filter-branch의 공식 지원 중단(Deprecated)과 치명적 결함
과거의 수많은 블로그와 스택오버플로우는 이 문제를 해결하기 위해 git filter-branch 명령어를 안내했습니다:
그러나 Git 공식 프로젝트는 Git 2.22(2019년)부터 git filter-branch의 사용을 공식적으로 중단(Deprecated)하였으며, 실행 시 터미널에 강력한 경고 문구를 출력합니다:
git filter-branch가 퇴출된 근본적인 이유는 다음과 같습니다:
1. 극단적인 성능 저하 (Excruciatingly Slow): 매 커밋마다 독립된 쉘 프로세스(Subshell)를 포크(Fork)하여 Git 인덱스를 초기화하고 파싱합니다. 커밋이 수천~수만 개에 달하는 저장소의 경우 필터링에 수 시간에서 며칠이 소요됩니다.
2. 히스토리 및 참조 훼손 (Data Mangling): 병합 커밋(Merge Commit)의 부모 링크를 잘못 처리하여 브랜치 구조를 엉망으로 만들거나, 서명된 태그(Annotated/Signed Tags)를 손상시키는 버그가 빈번합니다.
3. 불완전한 가비지 컬렉션: 필터링 후에도 원격 추적 브랜치(refs/original/)와 백업 참조를 남겨두어 git gc를 실행해도 로컬 .git 디렉토리 용량이 전혀 줄어들지 않는 문제가 발생합니다.
3) BFG Repo-Cleaner의 한계
BFG Repo-Cleaner는 git filter-branch의 느린 속도를 개선하기 위해 널리 사용되었던 Java 기반 오픈소스 도구입니다. 실행 속도는 빠르지만 다음과 같은 실무적 제약이 존재합니다:
- Java 런타임(JRE) 의존성: JVM이 설치되어 있지 않은 컨테이너나 경량 CI 환경에서 즉시 실행하기 번거롭습니다.
- 최신 커밋(HEAD) 보호 정책: BFG는 기본적으로 가장 최신 커밋(HEAD)에 존재하는 파일은 실수 방지를 위해 삭제하지 않고 보호합니다. 이로 인해 방금 대용량 파일을 커밋한 사용자가 BFG를 돌렸을 때 파일이 지워지지 않아 혼란을 초래합니다.
- 경로 리네임 및 세밀한 필터링 불가: 단순 파일명 일치나 크기 기반 삭제는 가능하지만, 복잡한 디렉토리 재구조화나 정규표현식 기반의 정밀한 이력 조작을 지원하지 않습니다.
2. 최신 공식 표준 해법: Git 공식 권장 git-filter-repo 아키텍처
Git 공식 커뮤니티와 메인테이너(Elijah Newren)가 공식 대체재로 지정하고 유지보수하는 도구가 바로 파이썬 기반의 git-filter-repo입니다. Git 공식 매뉴얼(man git-filter-branch)에서도 "Please migrate to git-filter-repo"라고 명시하고 있습니다.
1) git-filter-repo의 고속 스트리밍 아키텍처
git-filter-repo는 서브쉘을 반복 포크하는 대신, Git 코어 엔진의 초고속 I/O 인터페이스인 git fast-export와 git fast-import 스트림 파이프라인을 직접 제어합니다.
- 10배~100배 이상의 압도적 속도: 수만 개의 커밋이 누적된 대형 엔터프라이즈 모노레포도 단 수 초~수십 초 만에 전체 히스토리를 재작성합니다.
- 완벽한 DAG 정합성 보장: 머지 커밋 구조, 커밋 작성자 정보, 타임스탬프, 태그를 완벽하게 보존하며, 파일이 삭제되어 내용이 없어진 빈 커밋은 스마트하게 정리합니다.
- C-Python 인터페이스의 안전성: 파이썬 3 환경에서 단일 스크립트 또는 모듈 형태로 동작하여 환경 의존성이 매우 낮습니다.
2) 내장된 안전장치: 원격 저장소(origin) 자동 언링크 메커니즘
git-filter-repo의 가장 뛰어난 설계적 특징 중 하나는 "대참사 방지 안전장치"입니다.
히스토리를 재작성하면 커밋 해시(SHA)가 완전히 새로 생성됩니다. 이 상태에서 개발자가 무심코 일반 git push를 실행하거나 브랜치를 잘못 동기화하면 원격 저장소의 협업 브랜치가 파괴될 수 있습니다. 이를 원천 차단하기 위해 git-filter-repo는 작업이 성공하면 로컬 저장소에 등록되어 있던 원격 저장소 연결(origin remote)을 자동으로 삭제합니다.
따라서 필터링 후 로컬에서 저장소 용량 다이어트와 무결성을 완벽히 검증한 뒤, 관리자가 명시적으로 git remote add origin <URL>을 재등록하고 강제 푸시를 결정하도록 유도합니다.
3. 도구별 성능 및 기능 호환성 매트릭스
대용량 파일 제거 및 Git 히스토리 재작성에 사용되는 대표적인 4가지 접근법의 기술적 특징과 제약을 상세 비교합니다.
| 비교 항목 | git rm + 커밋 | git filter-branch (레거시) | BFG Repo-Cleaner | git-filter-repo (공식 표준) |
|---|---|---|---|---|
| 작업 대상 | 최신 작업 트리 (Working Tree) | 전체 히스토리 커밋 | 전체 히스토리 (HEAD 기본 제외) | 전체 히스토리 완벽 지원 |
| 과거 커밋 Blob 제거 | 불가능 (용량 그대로 유지) | 가능 (단, reflog 수동 정리 필요) | 가능 | 완전 영구 삭제 (초고속 GC) |
| 실행 속도 (1만 커밋 기준) | 수 밀리초 (무의미) | 수십 분 ~ 수 시간 (극도로 느림) | 수 초 ~ 수십 초 (빠름) | 수 초 이내 (가장 빠름) |
| 외부 런타임 의존성 | 없음 (기본 Git 내장) | 없음 (Bash/sh 서브쉘) | Java JRE (JVM 8 이상 필수) | Python 3 (단일 모듈) |
| Git 공식 프로젝트 권장 | X (해결책 아님) | 공식 비권장 (Deprecated) | X (서드파티 커뮤니티 도구) | 공식 권장 표준 (Official Standard) |
| 머지 커밋 / 태그 보존 | 영향 없음 | 파손 위험 높음 | 안정적이나 태그 처리 제한적 | 완벽한 정합성 및 자동 재작성 |
| 원격 안전장치 (Remote) | 없음 | 없음 | 없음 | origin 자동 제거로 대참사 방지 |
| 고급 필터링 기능 | 파일 삭제만 가능 | 쉘 스크립트 작성 (오류 취약) | 크기/단순 파일명 한정 | 경로, 크기, 정규식, 리네임 완비 |
| 실무 사용 권장 여부 | 푸시 거부 미해결 | 사용 절대 금지 | Java 환경에서 제한적 사용 | 모든 프로덕션 환경 강력 권장 |
4. 실무 프로덕션 복구 명령어 명세 (단계별 엔지니어링 절차)
대용량 파일로 인해 푸시가 차단되었을 때, 저장소를 안전하게 백업하고 git-filter-repo를 통해 히스토리를 완벽히 정제한 후 원격 저장소에 반영하는 6단계 정석 파이프라인입니다.
Step 0. 필수 안전장치: 원본 저장소 미러 백업
히스토리 재작성은 되돌릴 수 없는 파괴적 작업(Destructive Operation)입니다. 반드시 작업 시작 전 전체 저장소를 별도 디렉토리에 백업합니다.
Step 1. OS별 git-filter-repo 설치
git-filter-repo는 Python 3(3.6 이상)과 Git(2.22 이상)이 설치된 환경에서 구동됩니다.
Step 2. 히스토리 내 숨겨진 대용량 파일(Top Blobs) 정밀 추적
어떤 파일이 저장소 용량을 차지하고 있는지 정확한 파일 경로와 크기를 파악해야 합니다.
1) Bash 파이프라인을 통한 상위 10개 대용량 파일 검색
Git의 내부 객체 목록을 크기 순으로 정렬하여 출력하는 스크립트입니다:
실행 결과 예시:
2) git-filter-repo 자체 분석 리포트 생성
git-filter-repo는 저장소 전체의 상태를 분석하는 강력한 --analyze 옵션을 제공합니다:
Step 3. 대용량 파일 히스토리 완전 영구 삭제
상황에 따라 특정 파일 경로를 지정하여 삭제하거나, 특정 용량 이상의 모든 객체를 일괄 삭제할 수 있습니다.
[시나리오 A] 특정 대용량 파일 또는 디렉토리 지정 완전 삭제
--invert-paths 플래그는 지정한 경로를 제외한 나머지 모든 것을 유지하라는 의미이므로, 해당 경로의 파일이 전체 히스토리에서 완벽히 영구 삭제됩니다.
[시나리오 B] 특정 크기(예: 50MB, 100MB) 초과 파일 일괄 자동 스트립
GitHub의 경고 기준인 50MB 또는 하드 리밋인 100MB를 초과하는 모든 과거 블롭 객체를 한 번에 소거하려면 --strip-blobs-bigger-than 옵션을 사용합니다:
주의:--strip-blobs-bigger-than은 파일을 삭제하는 대신 빈 파일(0 바이트) 또는 플레이스홀더로 교체하여 커밋 트리의 무결성을 유지하면서 바이너리 본체만 제거합니다.
Step 4. 로컬 가비지 컬렉션(GC) 및 저장소 다이어트 강제 압축
git-filter-repo 작업이 완료되면 과거 커밋들은 제거되지만, 고아 객체(Dangling/Unreachable Blobs)가 로컬 reflog에 임시 보관되어 있을 수 있습니다. 다음 명령어로 디스크 공간을 즉시 회수합니다:
실행 후 .git 디렉토리 크기를 확인하면 기가바이트 단위였던 저장소 크기가 수십 메가바이트 수준으로 드라마틱하게 줄어든 것을 확인할 수 있습니다.
Step 5. 원격 저장소 재연결 및 원격 강제 푸시 (--force --all --tags)
앞서 설명한 바와 같이, git-filter-repo는 안전을 위해 원격 저장소(origin) 설정을 자동으로 제거합니다. 원격 주소를 다시 연결하고 정제된 히스토리를 강제로 반영합니다.
5. 자주 겪는 4대 치명적 에러 및 트러블슈팅
실무에서 git-filter-repo를 적용할 때 반드시 한 번은 맞닥뜨리게 되는 치명적 예외 상황과 완벽한 종결 해결책입니다.
Error 1. Aborting: Refusing to overwrite existing target ... 또는 not a fresh clone
- 발생 증상 및 콘솔 에러 로그:
- 원인 분석:
git-filter-repo는 기본적으로 작업 디렉토리가 오염되지 않은 "깨끗한 클론(Fresh Clone)" 상태에서 실행되는 것을 전제로 합니다. 커밋되지 않은 미추적 파일(Untracked files)이 있거나, 이미 한 번 필터링을 수행한 이력이 있는 디렉토리에서 재실행할 경우 기존 데이터 유실을 방지하기 위해 프로세스를 즉시 중단합니다. - 해결 방법:
작업 트리에 커밋되지 않은 중요한 변경사항이 없는지
git status로 확인한 후,--force플래그를 추가하여 실행합니다:
Error 2. 필터링 후 푸시했는데도 remote: error: GH001 재발생
- 발생 증상 및 콘솔 에러 로그:
- 원인 분석:
로컬
main브랜치의 히스토리는 정상적으로 정제되었으나, 다음 두 가지 원인 중 하나로 인해 대용량 Blob이 여전히 푸시 패키지에 포함된 경우입니다: - 과거 릴리즈에 달아둔 Git 태그(Annotated/Lightweight Tags)가 대용량 파일이 존재하던 과거 커밋 해시를 여전히 가리키고 있는 경우.
- 로컬 또는 원격에 존재하는 다른 브랜치(
develop,feature/xyz)에 해당 파일이 포함된 커밋이 남아 있는 경우. - 해결 방법:
- 원격에만 남아 있는 불필요한 오래된 브랜치나 태그를 원격 저장소에서 직접 삭제합니다:
- 로컬의 모든 브랜치와 태그가
git-filter-repo로 함께 재작성되었는지 확인하고, 단일 브랜치 푸시(git push origin main)가 아닌--all과--tags를 반드시 함께 강제 푸시합니다:
Error 3. 협업 팀원의 로컬에서 발생하는 머지 충돌 대참사 (Updates were rejected)
- 발생 증상 및 콘솔 에러 로그:
- 원인 분석:
히스토리를 재작성하면 최초 커밋부터 최신 커밋까지 모든 커밋 해시(SHA)가 완전히 새로 바뀝니다. 원격 저장소에 강제 푸시된 직후, 다른 동료 개발자가 평소 습관대로
git pull을 실행하면 Git은 "전혀 관계없는 두 개의 평행 우주 히스토리"를 하나로 합치려고 시도합니다. 이 과정에서 팀원이 가지고 있던 과거 커밋의 대용량 파일이 병합(Merge)을 통해 되살아나고 다시 푸시되는 악순환이 발생합니다. - 해결 방법 (팀원 긴급 대응 수칙): 히스토리 재작성 후에는 팀원들에게 "절대로 git pull을 실행하지 말 것"을 즉시 공지하고, 아래의 2단계 초기화 명령을 수행하도록 안내해야 합니다:
Error 4. 대용량 파일의 형상 관리가 필수적인 경우: Git LFS(Large File Storage) 정석 전환
- 발생 증상 및 배경:
인공지능 모델 체크포인트(
.pt,.safetensors), 디자인 원본 에셋(.psd), 대용량 바이너리 라이브러리(.so,.dll)는 단순 삭제할 수 없고 형상 관리가 반드시 필요합니다. 이를 일반 Git 저장소에 올리면 GitHub 용량 초과가 발생하므로 Git 공식 확장 규격인 Git LFS로 전환해야 합니다. - 해결 방법:
- Git LFS 패키지 설치 및 저장소 초기화:
- 대상 대용량 확장자 트래킹 등록:
- 이미 커밋되어 버린 기존 히스토리를 LFS 포인터로 일괄 마이그레이션: 새 파일뿐만 아니라 이미 과거 커밋에 일반 Blob으로 박혀 있는 대용량 파일들을 Git LFS 포인터(130바이트 텍스트 메타데이터)로 소급 전환해야 푸시 거부가 해결됩니다:
6. 저장소 용량 다이어트 검증 및 5단계 최종 복구 체크리스트
모든 작업이 완료된 후 실제로 저장소 용량이 정상적으로 감축되었는지 확인하고, 프로덕션 환경에 안전하게 배포하기 위한 최종 검증 절차입니다.
1) 저장소 다이어트 결과 검증 CLI
로컬 .git 디렉토리 실측 용량 확인
잔여 50MB 이상 대형 객체 검사 스크립트
필터링 후 저장소 내에 50MB를 초과하는 객체가 단 하나라도 남아 있는지 즉시 확인합니다:
위 명령어를 실행했을 때 아무런 결과도 출력되지 않아야 완벽히 다이어트가 완료된 것입니다.
GitHub 원격 저장소 용량 반영 주기
GitHub 웹 UI의 Settings -> General에 표시되는 저장소 용량(Repository size)은 강제 푸시 직후 즉시 갱신되지 않습니다. GitHub 백엔드의 가비지 컬렉션(GC) 데몬이 주기적으로 실행된 후(통상 24시간 이내) 반영됩니다. 즉각적인 크기 갱신이 필요하다면 GitHub Support에 저장소 캐시 GC 요청 티켓을 접수할 수 있습니다.
2) 5초 최종 복구 체크리스트 (5-Step Checklist)
프로덕션 저장소의 대용량 커밋 거부 이슈를 종결짓기 전, 다음 5가지 핵심 항목을 반드시 점검하십시오.
- 1. 안전 백업본 생성 확인: 작업 전
git clone --mirror또는 로컬 디렉토리 전체 압축 백업을 별도 공간에 안전하게 보관했는가? - 2.
git-filter-repo파일 소거 확인: 대상 파일 경로 또는--strip-blobs-bigger-than필터링이 완료되어 로컬 대용량 객체 검색 결과가 0건인가? - 3. Aggressive GC 디스크 공간 회수:
git reflog expire --expire=now --all및git gc --prune=now --aggressive를 실행하여.git폴더 용량이 유의미하게 축소되었는가? - 4. 원격 리모트 재등록 및 강제 푸시:
git remote add origin으로 원격 URL을 재연결하고,--force --all과--force --tags를 모두 실행하여 누락된 브랜치/태그가 없는가? - 5. 팀원 로컬 동기화 가이드 전파: 함께 협업하는 팀원들에게
git pull을 금지하고git fetch origin && git reset --hard origin/<branch>로 동기화하도록 긴급 전파를 완료했는가?