mLog 유령 캐릭터가 Git 커밋 기록에서 대용량 객체를 제거해 저장소를 정리하는 3D 일러스트

Git remote를 지웠는데 용량이 그대로인 이유: .git 대용량 파일 진단과 안전한 정리

개발 Git·GitLab 2026년 9월 3일

더 이상 사용하지 않는 원격 저장소를 분리하려고 git remote remove origin을 실행했습니다. git remote -v를 확인하니 연결은 사라졌는데, 이상하게도 프로젝트 폴더의 용량은 거의 그대로였습니다. 현재 작업 폴더에서 큰 파일을 지우고 .gitignore에도 추가했지만 .git 폴더는 여전히 컸습니다.

처음에는 가비지 컬렉션만 강제로 실행하면 해결될 것 같았습니다. 그런데 git gc를 실행해도 기대만큼 줄지 않았고, 큰 객체를 찾기 위해 복사한 긴 파이프라인은 사용 중인 셸에 따라 동작하지 않았습니다. 새 저장소를 만들고 커밋 시간을 과거로 바꾼 뒤 필요한 커밋만 cherry-pick하는 방법도 떠올렸지만, 이번에는 deleted by us 충돌이 발생했습니다.

돌아보면 각각의 명령이 실패한 것이 아니었습니다. 해결하려는 문제와 명령의 역할이 달랐습니다.

  • remote remove는 연결 정보를 지웁니다.
  • gc는 참조되지 않는 객체만 정리합니다.
  • 커밋 날짜는 커밋의 부모 관계를 만들지 않습니다.
  • cherry-pick은 파일을 통째로 복사하는 대신 기존 이력 위에 변경분을 적용합니다.
  • sparse-checkout은 작업 트리를 줄이지만, 단독으로 과거 객체 다운로드를 막는 기능은 아닙니다.

이 글에서는 이 차이를 먼저 정리한 다음, 저장소 용량을 실제로 차지하는 대상을 찾고 목적에 맞는 정리 방법을 선택합니다. 가장 안전한 재클론부터 원격 히스토리를 다시 쓰는 git-filter-repo까지, 작업 순서와 실패 지점도 함께 살펴보겠습니다.

3줄 요약

  • git remote remove는 원격 주소와 추적 설정을 없애는 명령이지, 이미 받은 Git 객체를 삭제하는 명령이 아닙니다.
  • 로컬 디스크 공간만 줄이는 목적이라면 재클론·shallow clone·partial clone이 히스토리 재작성보다 안전할 수 있습니다.
  • 원격 저장소 자체에서 대용량 파일을 없애려면 git-filter-repo로 히스토리를 재작성해야 하며, 백업과 팀 전체 재클론 계획이 필요합니다.

목차

  1. remote를 지웠는데 왜 용량은 그대로일까
  2. Git이 데이터를 보관하는 방식을 먼저 이해하자
  3. 처음 시도한 방법들이 해결책이 아니었던 이유
  4. 삭제 전에 반드시 확인할 것
  5. 실제 용량을 차지하는 대상을 찾는 방법
  6. 목적에 맞는 해결 방법 선택하기
  7. 가장 안전한 로컬 정리: 새로 클론하기
  8. shallow·partial·sparse clone의 차이
  9. 참조가 끊긴 로컬 객체만 정리하기
  10. 대용량 파일을 히스토리에서 완전히 삭제하기
  11. Windows·macOS·Linux에서 헷갈리는 부분
  12. 정리가 제대로 끝났는지 검증하기
  13. 자주 마주치는 실패와 해결 방법
  14. 팀 저장소라면 추가로 해야 할 일
  15. 저장소가 다시 커지는 것을 막는 방법
  16. 최종 체크리스트

1. remote를 지웠는데 왜 용량은 그대로일까

결론부터 말하면 remote는 저장 공간이 아니라 다른 저장소로 가는 연결 정보입니다.

보통 origin에는 가져오기와 밀어넣기에 사용할 URL, refspec 같은 설정이 묶여 있습니다. 다음 명령은 origin이라는 원격 설정과 그 원격의 추적 브랜치를 제거합니다.

git remote remove origin

Git 공식 문서도 remote remove가 원격 설정과 remote-tracking branch를 제거한다고 설명합니다. 반면 로컬 브랜치, 태그, 커밋, 파일 내용이 들어 있는 객체 데이터베이스까지 지운다고 하지는 않습니다. 이미 내려받은 객체가 로컬 브랜치나 태그에서 계속 도달 가능하다면 Git은 그것을 정상적인 프로젝트 이력으로 봅니다.

예를 들어 origin/main과 로컬 main이 같은 커밋을 가리키는 상태에서 origin을 제거해도, 로컬 main이 그 커밋과 모든 조상을 계속 가리킵니다. 따라서 해당 커밋 안에 있던 대용량 blob도 살아 있습니다.

여기서 한 가지 순서 문제도 생깁니다.

git remote prune origin

이 명령은 원격에서 이미 삭제된 오래된 추적 브랜치를 정리할 때 사용합니다. origin을 먼저 제거한 뒤 실행하면 정리할 원격 자체가 없으므로 오류가 납니다. 그리고 remote remove는 해당 원격 추적 브랜치를 이미 제거하므로, 제거 이후에 remote prune origin을 다시 실행할 이유도 없습니다.

즉, remote remove가 정상적으로 끝났는데 용량이 줄지 않은 것은 이상 현상이 아닙니다. 연결 제거와 객체 삭제는 서로 다른 작업입니다.

목차로 돌아가기

2. Git이 데이터를 보관하는 방식을 먼저 이해하자

현재 화면에 보이는 파일만 생각하면 “큰 ZIP 파일을 삭제했으니 용량도 줄어야 한다”는 결론이 자연스럽습니다. 하지만 Git 저장소에는 현재 파일 외에 과거 버전이 함께 들어 있습니다.

작업 폴더와 다음 커밋

  • 작업 트리는 지금 편집하고 있는 파일입니다. 여기서 파일을 지워도 과거 커밋에는 남을 수 있습니다.
  • 인덱스는 다음 커밋에 담을 변경 사항입니다. 객체를 지우는 곳이 아니라 다음 스냅샷을 준비하는 영역입니다.

실제 히스토리를 구성하는 객체

  • commit은 작성자·시간·메시지와 부모, tree를 가리킵니다. 시간보다 부모 연결이 Git의 계보를 만듭니다.
  • tree는 특정 시점의 디렉터리 구조와 파일 객체를 연결합니다.
  • blob은 파일 내용입니다. 동영상·압축 파일·DB 덤프가 저장소를 크게 만드는 주원인이 됩니다.

객체를 살려 두는 참조와 저장 형식

  • ref는 브랜치·태그 등이 가리키는 커밋입니다. ref에서 도달 가능한 객체는 GC가 삭제하지 않습니다.
  • reflog는 브랜치와 HEAD가 이동한 기록으로, 삭제하거나 reset한 커밋을 일정 기간 복구할 수 있게 붙잡습니다.
  • packfile은 여러 객체를 압축한 묶음이며 .git/objects/pack에서 큰 파일로 보이는 경우가 많습니다.

핵심은 **도달 가능성(reachability)**입니다. 브랜치나 태그가 커밋을 가리키고, 그 커밋에서 tree와 blob으로 연결된다면 해당 객체는 사용 중입니다. git gc는 이런 객체를 임의로 지우지 않습니다.

현재 커밋에서 큰 파일을 삭제해도 그 파일을 추가했던 과거 커밋이 남아 있으면 blob은 계속 도달 가능합니다. 모든 일반 clone이 그 과거를 가져와야 하므로 새로 clone해도 저장소가 다시 커집니다. 이 경우에는 단순 정리가 아니라 히스토리 재작성이 필요합니다.

반대로 브랜치를 삭제하거나 reset한 뒤 어떤 ref에서도 도달하지 못하는 객체가 생겼다면, reflog와 유예 기간이 끝난 후 가비지 컬렉션 대상이 될 수 있습니다. 이 둘을 구분해야 합니다.

목차로 돌아가기

3. 처음 시도한 방법들이 해결책이 아니었던 이유

실패 1: git remote remove origin만 실행했다

원격 연결은 제대로 끊어졌습니다. 다만 로컬 브랜치가 과거 커밋을 계속 가리키므로 객체는 그대로 남았습니다. 디스크 정리 명령이 아니라 연결 관리 명령을 실행한 셈입니다.

실패 2: 큰 파일을 지우고 .gitignore에 추가했다

.gitignore는 앞으로 추적하지 않을 파일을 정하는 규칙입니다. 이미 커밋된 과거 blob에 소급 적용되지 않습니다. 현재 브랜치에서 git rm으로 파일을 삭제해도 이전 커밋의 내용은 남습니다.

실패 3: 원인 확인 전에 git gc --prune=now --aggressive부터 실행했다

git gc는 객체를 정리하고 pack을 최적화하지만, 브랜치나 태그에서 도달 가능한 대용량 blob은 삭제하지 않습니다. --aggressive는 더 많은 시간과 CPU를 들여 재압축하는 옵션이지, 살아 있는 이력을 없애는 옵션이 아닙니다.

--prune=now는 기본 유예 기간을 없애고 도달 불가능한 객체를 즉시 버립니다. 복구 여지가 줄어들고 다른 Git 프로세스가 객체를 쓰는 시점과 겹칠 위험도 있으므로, “일단 한번 실행해 보는 명령”으로 사용하면 안 됩니다.

실패 4: 커밋 날짜를 과거로 바꾼 뒤 cherry-pick하려 했다

Git 커밋에는 Author Date와 Committer Date가 있지만, 시간순으로 부모가 결정되지는 않습니다. 커밋의 계보는 부모 커밋 ID로 연결됩니다. 따라서 날짜를 과거로 바꿔도 새로 git init한 저장소와 기존 원격 저장소 사이에 공통 조상이 생기지 않습니다.

cherry-pick은 대상 커밋의 완성된 파일을 복사하는 기능도 아닙니다. 부모 대비 변경분을 현재 이력 위에 적용합니다. 대상 커밋이 수정하려는 파일이 현재 쪽에는 없거나 서로 다른 계보에서 상태가 달라졌다면 deleted by us, modify/delete 같은 충돌이 날 수 있습니다.

용량을 줄이는 일이 목적이라면 커밋 날짜 변경과 cherry-pick은 우회로입니다. 원격 상태를 그대로 받고 싶다면 새 clone이 낫고, 일부 큰 객체만 과거에서 제거하려면 히스토리 필터링이 맞습니다.

실패 5: sparse-checkout만 켜면 다운로드도 줄어든다고 생각했다

sparse-checkout작업 트리에 펼칠 파일의 범위를 줄입니다. 많은 파일 중 일부 디렉터리만 보고 싶을 때 효과적이지만, 이것만으로 .git 안의 과거 객체를 모두 안 받는다고 보장하지는 않습니다.

초기 전송량까지 줄이려면 서버가 지원하는 partial clone의 --filter=blob:none을 함께 사용해야 합니다. 작업 트리 축소와 객체 전송 축소는 별개의 기능입니다.

목차로 돌아가기

4. 삭제 전에 반드시 확인할 것

히스토리 재작성이나 즉시 prune은 되돌리기 어렵습니다. 명령을 복사하기 전에 먼저 현재 저장소가 무엇을 가지고 있는지 기록합니다.

git status --short
git remote -v
git branch --all
git tag --list
git rev-parse --show-toplevel
git rev-parse --git-dir
git rev-parse --git-common-dir
git count-objects -vH

git rev-parse --git-common-dir은 worktree를 쓰는 환경에서 특히 중요합니다. 여러 worktree가 객체 저장소를 공유할 수 있으므로, 눈앞의 작업 폴더 하나만 보고 삭제 범위를 판단하면 안 됩니다.

다음 항목도 확인합니다.

  • 미커밋 변경과 미추적 파일이 있는가?
  • 아직 원격에 push하지 않은 로컬 브랜치가 있는가?
  • 삭제하려는 파일이 현재 버전에는 필요한가?
  • 같은 파일이 과거에 다른 경로와 이름으로 존재했는가?
  • 저장소를 사용하는 동료, CI, 배포 서버, 서브모듈이 있는가?
  • 보호 브랜치와 서명된 태그가 있는가?
  • 문제의 파일이 일반 Git blob인가, Git LFS 객체인가?
  • 비밀번호·토큰이라면 이미 폐기하거나 교체했는가?

가능하면 원본 폴더를 그대로 보관하고 별도의 새 clone에서 작업합니다. 추가로 모든 ref에서 도달 가능한 커밋을 한 파일에 보관하려면 bundle을 만들 수 있습니다.

git bundle create ../repository-before-cleanup.bundle --all
git bundle verify ../repository-before-cleanup.bundle

다만 bundle은 작업 트리의 미커밋 파일, 인덱스, 저장소별 설정, hook까지 백업하는 기능이 아닙니다. git status가 깨끗한지 확인하고, 미커밋 파일은 별도로 보관해야 합니다.

비밀 정보라면 순서가 다릅니다. 토큰·비밀번호·개인 키가 커밋되었다면 히스토리 정리보다 자격 증명 폐기와 교체가 먼저입니다. 커밋을 지워도 다른 clone, fork, 캐시에 사본이 남을 수 있습니다.

목차로 돌아가기

5. 실제 용량을 차지하는 대상을 찾는 방법

5-1. Git 객체 통계부터 확인한다

운영체제에 관계없이 가장 먼저 실행하기 좋은 명령은 다음과 같습니다.

git count-objects -vH

현재 브랜치·태그 등에서 도달 가능한 객체의 디스크 사용량과 reflog까지 포함한 사용량을 비교하려면 다음 명령도 유용합니다.

git rev-list --disk-usage=human --objects --all
git rev-list --disk-usage=human --objects --all --reflog

두 값의 차이가 크다면 현재 이력에서는 끊겼지만 reflog가 아직 붙잡고 있는 객체가 많을 가능성이 있습니다. 다만 이 결과만 보고 곧바로 reflog를 만료시키지 말고, 복구할 작업이 없는지 먼저 확인합니다.

주요 출력은 다음처럼 읽습니다.

  • size: pack에 들어가지 않은 loose object의 용량
  • in-pack: packfile 안의 객체 수
  • packs: packfile 수
  • size-pack: packfile이 차지하는 용량
  • prune-packable: pack과 중복되어 정리할 수 있는 loose object 수
  • garbage, size-garbage: 유효한 객체나 pack이 아닌 데이터

대부분의 용량이 size-pack에 있다면 단순히 .git/objects/pack/*.pack을 지우면 안 됩니다. 그 파일은 여러 커밋의 실제 데이터베이스입니다. 반드시 Git 명령으로 어떤 blob이 큰지 추적해야 합니다.

5-2. 가장 편한 분석 방법은 git filter-repo --analyze

git-filter-repo가 설치되어 있다면 다음 명령은 저장소를 변경하지 않고 분석 보고서를 만듭니다.

git filter-repo --analyze

보고서에는 경로·디렉터리·확장자·blob ID별 크기와 이름 변경 정보가 포함됩니다. 큰 파일을 정확히 알고 있다고 생각해도 먼저 분석하는 편이 좋습니다. 같은 파일이 과거에 다른 이름으로 이동했다면 현재 경로 하나만 필터링해서는 예전 blob이 남을 수 있기 때문입니다.

설치는 공식 프로젝트가 안내하는 패키지 관리자를 사용할 수 있습니다.

# macOS
brew install git-filter-repo

# Windows·macOS·Linux 공통 선택지
pipx install git-filter-repo

설치 뒤에는 버전을 확인합니다.

git filter-repo --version

5-3. Git Bash·macOS·Linux에서 큰 blob을 직접 찾는 방법

도구를 추가로 설치하기 어렵다면 다음 파이프라인으로 모든 ref에서 도달 가능한 blob을 크기순으로 볼 수 있습니다.

git rev-list --objects --all |
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
  awk '$1 == "blob" {print $3 "\t" $2 "\t" substr($0, index($0, $4))}' |
  sort -n |
  tail -20

출력은 바이트 크기, 객체 ID, 경로 순입니다. 원래 자주 공유되는 verify-pack | sort | tail | awk 조합은 POSIX 계열 명령에 의존하며, .idx pack이 아직 없거나 PowerShell·CMD에서 실행하면 실패할 수 있습니다. 또한 객체 ID와 경로를 연결하는 단계가 빠지면 “큰 객체는 찾았는데 어떤 파일인지 모르는” 상태가 됩니다.

여기서 objectsize는 blob의 압축 전 논리 크기입니다. delta 압축이 적용된 packfile 안의 실제 점유량과 정확히 같지는 않으므로 삭제 후보를 찾는 용도로 사용하고, 최종 절감량은 정리 전후 git count-objects -vH와 새 clone 크기로 확인합니다.

PowerShell에서 같은 목록이 꼭 필요하다면 셸 명령을 섞지 말고 다음처럼 PowerShell 객체로 변환합니다.

git rev-list --objects --all |
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
  ForEach-Object {
    $part = $_ -split ' ', 4
    if ($part[0] -eq 'blob') {
      [pscustomobject]@{
        Bytes  = [int64]$part[2]
        Object = $part[1]
        Path   = $part[3]
      }
    }
  } |
  Sort-Object Bytes -Descending |
  Select-Object -First 20

환경별 파이프라인을 관리하고 싶지 않다면 git filter-repo --analyze를 공통 진단법으로 쓰는 편이 가장 덜 깨집니다.

목차로 돌아가기

6. 목적에 맞는 해결 방법 선택하기

가장 강한 명령부터 실행하지 말고, 무엇을 줄이려는지 먼저 고릅니다.

원격이나 공유 이력을 바꾸지 않는 선택

  • 연결만 끊기: git remote remove를 사용합니다. 위험도는 낮고 원격 히스토리는 바뀌지 않습니다.
  • 오래 쓴 로컬 clone만 정리: 새 폴더에 일반 clone을 만듭니다. 가장 먼저 검토할 안전한 방법입니다.
  • 최신 커밋 몇 개만 필요: shallow clone을 사용합니다. 대신 과거 이력을 이용하는 작업에 제약이 생깁니다.
  • 파일 내용은 필요할 때만 받기: partial clone을 사용합니다. 서버가 필터 기능을 지원해야 합니다.
  • 일부 디렉터리만 펼치기: partial clone과 sparse checkout을 결합합니다.

객체나 공유 이력을 실제로 없애는 선택

  • 참조가 끊긴 로컬 객체를 즉시 제거: reflog 만료와 GC가 필요합니다. 복구 기록을 잃는 중간 위험 작업입니다.
  • 모든 clone에서 특정 대용량 파일 제거: git-filter-repo로 히스토리를 재작성합니다. 관련 커밋 ID가 바뀌는 고위험 작업입니다.
  • 노출된 비밀 정보 제거: 자격 증명을 먼저 폐기한 다음 히스토리를 재작성합니다. 호스팅 서비스 지원까지 필요할 수 있습니다.

로컬 디스크만 문제라면 재클론을 먼저 검토합니다. 원격의 도달 가능한 과거 자체가 문제일 때만 히스토리를 재작성합니다. 이 원칙 하나만 지켜도 불필요한 force push와 데이터 손실 위험을 크게 줄일 수 있습니다.

목차로 돌아가기

7. 가장 안전한 로컬 정리: 새로 클론하기

오래 사용한 clone에는 삭제한 브랜치의 reflog, 도달 불가능한 객체, 오래된 pack, 여러 실험 흔적이 남을 수 있습니다. 원격 저장소가 정상이고 로컬 공간만 확보하려는 목적이라면 깨끗한 clone을 비교해 보는 것이 가장 명확합니다.

REPOSITORY_URL='https://git.example.com/owner/repository.git'
git clone "$REPOSITORY_URL" repository-clean
cd repository-clean
git count-objects -vH
git status

새 clone에서 다음 항목을 확인합니다.

  1. 필요한 브랜치와 태그가 보이는가?
  2. 애플리케이션이 빌드되고 테스트가 통과하는가?
  3. 기존 clone에만 있던 로컬 설정과 hook이 필요한가?
  4. 서브모듈과 Git LFS 파일이 정상적으로 내려오는가?
  5. 새 clone의 .git 용량이 실제로 더 작은가?

새 clone이 크게 줄었다면 기존 로컬 저장소에만 불필요한 객체나 참조가 쌓였을 가능성이 큽니다. 반대로 새 clone도 거의 같은 크기라면 대용량 객체가 원격의 정상 히스토리에서 도달 가능한 상태입니다. 이때는 재클론을 반복해도 해결되지 않습니다.

기존 폴더는 새 clone의 빌드와 테스트가 끝나기 전에 지우지 않습니다. 특히 미추적 파일, .env, IDE 설정, 저장소 내부 hook은 일반 clone으로 복원되지 않을 수 있습니다.

목차로 돌아가기

8. shallow·partial·sparse clone의 차이

대형 저장소를 전부 받을 필요가 없다면 히스토리를 파괴적으로 다시 쓰지 않고도 로컬 사용량을 줄일 수 있습니다.

  • shallow clone의 --depth=1: 커밋 히스토리를 줄입니다. 최신 일부 이력과 그 범위에서 필요한 blob을 받습니다.
  • partial clone의 --filter=blob:none: 초기 blob 전송을 줄입니다. commit과 tree는 유지하고 파일 내용은 필요할 때 받습니다.
  • sparse-checkout: 작업 트리의 파일 수를 줄입니다. 히스토리를 바꾸지 않고 선택한 경로만 펼칩니다.

최신 상태만 필요하다면 shallow clone

REPOSITORY_URL='https://git.example.com/owner/repository.git'
git clone --depth=1 --single-branch --branch main "$REPOSITORY_URL" repository-shallow

다운로드는 작고 빠르지만 과거 git log, blame, bisect, 오래된 태그나 브랜치 작업에 제한이 생깁니다. 이미 전체 이력을 받은 저장소에 git fetch --depth 1만 실행한다고 과거 pack이 자동으로 깔끔하게 사라지는 것으로 생각해서는 안 됩니다. 용량 절감이 목적이면 새 shallow clone을 만들어 비교하는 편이 확실합니다.

커밋 구조는 필요하다면 partial clone

REPOSITORY_URL='https://git.example.com/owner/repository.git'
git clone --filter=blob:none "$REPOSITORY_URL" repository-partial

blob:none은 처음에는 파일 내용을 생략하고 필요할 때 원격에서 받습니다. 서버가 partial clone 필터를 지원해야 하며, checkout이나 diff 과정에서 필요한 blob은 다시 다운로드됩니다.

특정 폴더만 작업한다면 partial + sparse

REPOSITORY_URL='https://git.example.com/owner/repository.git'
git clone --filter=blob:none --sparse "$REPOSITORY_URL" repository-sparse
cd repository-sparse
git sparse-checkout set src backend

이 조합은 초기 blob 전송과 작업 트리 범위를 함께 줄입니다. sparse-checkout만 켰을 때보다 “다운로드 용량을 줄인다”는 목적에 더 정확합니다.

목차로 돌아가기

9. 참조가 끊긴 로컬 객체만 정리하기

큰 객체가 어떤 브랜치와 태그에서도 도달 가능하지 않고, 원본을 별도로 보관했으며, 복구할 필요도 없다는 사실을 확인했다면 reflog와 가비지 컬렉션을 검토할 수 있습니다.

먼저 일반 정리를 실행하고 다시 측정합니다.

git gc
git count-objects -vH

Git은 기본적으로 최근의 도달 불가능한 객체에 유예 기간을 둡니다. 즉시 정리가 꼭 필요하다면 아래 명령이 있지만, 이 시점부터 reflog를 통한 복구 가능성이 크게 줄어듭니다.

# 파괴적 작업: 백업과 참조 확인 후에만 실행
git reflog expire --expire=now --all
git gc --prune=now

주의할 점은 세 가지입니다.

  1. 로컬 브랜치·태그·stash 등에서 여전히 도달 가능한 객체는 삭제되지 않습니다.
  2. --aggressive를 붙여도 살아 있는 대용량 blob이 없어지지는 않습니다. 재압축 시간이 크게 늘 수 있어 기본 절차에서는 제외하는 편이 낫습니다.
  3. 다른 Git 명령이나 IDE의 백그라운드 작업이 객체를 쓰는 동안 --prune=now를 실행하지 않습니다.

위 명령 뒤에도 크기가 거의 같다면 GC가 실패한 것이 아니라, 큰 객체가 아직 히스토리에서 정상적으로 참조되고 있을 가능성이 큽니다. 다음 단계인 히스토리 재작성이 필요한지 판단해야 합니다.

목차로 돌아가기

10. 대용량 파일을 히스토리에서 완전히 삭제하기

이 단계는 모든 관련 커밋의 ID를 바꾸는 히스토리 재작성입니다. 이미 공유한 저장소라면 개인 정리 작업이 아니라 팀 마이그레이션으로 취급해야 합니다.

Git 자체의 git filter-branch 문서는 안전성과 성능 문제 때문에 사용을 권장하지 않고 git filter-repo 같은 대안을 안내합니다. 따라서 오래된 예제의 filter-branch를 그대로 복사하기보다 git-filter-repo를 사용합니다.

10-1. push를 멈추고 새 clone을 만든다

팀에 작업 시간을 공지하고, 정리 중 새 커밋이 원격에 들어오지 않게 합니다. 원본 작업 폴더에서 바로 실행하지 말고 새 clone을 만듭니다.

REPOSITORY_URL='https://git.example.com/owner/repository.git'
git clone "$REPOSITORY_URL" repository-cleanup
cd repository-cleanup
git status
git remote -v
git count-objects -vH

로컬 경로의 저장소를 복제한다면 hard link로 객체를 공유하지 않도록 --no-local을 사용합니다.

git clone --no-local /path/to/source-repository repository-cleanup

git-filter-repo가 “fresh clone이 아니다”라며 중단한다면 안전장치가 동작한 것입니다. 습관적으로 --force를 붙이지 말고 새 clone인지, 경로를 제대로 선택했는지부터 확인합니다.

10-2. 변경 없이 먼저 분석한다

git filter-repo --analyze

경로별 누적 크기와 이름 변경 이력을 확인하고, 삭제할 대상을 목록으로 만듭니다. “예전에 삭제된 파일”과 “이제 필요 없는 파일”은 같은 뜻이 아닙니다. 보고서에 등장했다는 이유만으로 일괄 삭제하지 않습니다.

10-3. 정확한 경로를 모든 이력에서 제거한다

예를 들어 assets/archive.zip을 모든 히스토리에서 없애려면 다음과 같이 실행합니다.

git filter-repo \
  --path 'assets/archive.zip' \
  --invert-paths

과거에 파일명이 바뀌었다면 이전 경로도 모두 지정합니다.

git filter-repo \
  --path 'backup/old-archive.zip' \
  --path 'assets/archive.zip' \
  --invert-paths

경로가 아니라 크기로 일괄 제거하는 옵션도 있습니다.

git filter-repo --strip-blobs-bigger-than 100M

이 명령은 “필요한가”가 아니라 “100MB보다 큰가”만 판단합니다. 정상적인 디자인 자산이나 배포 파일도 함께 사라질 수 있으므로, 원인 파일을 특정할 수 있다면 경로 기반 제거를 우선합니다.

비밀 정보 제거가 목적이고 지원 버전을 사용한다면 --sensitive-data-removal 모드를 검토할 수 있습니다. 하지만 토큰 교체와 호스팅 서비스의 캐시·fork 정리는 별도 작업입니다.

10-4. 원격에 올리기 전에 검증한다

git status
git fsck --full
git count-objects -vH
git log --all -- 'assets/archive.zip'
git filter-repo --analyze

삭제한 경로의 로그가 남지 않는지, 분석 보고서에서 대용량 blob이 사라졌는지 확인합니다. 이어서 애플리케이션의 빌드와 테스트를 실행합니다. 브랜치 수와 태그 수도 작업 전 기록과 비교합니다.

git-filter-repo는 실수로 정리된 히스토리를 원격에 덮어쓰지 못하도록 origin을 제거할 수 있습니다. 오류가 아니라 안전 동작일 수 있습니다. 모든 검증을 마친 뒤 정확한 URL을 다시 등록합니다.

git remote add origin "$REPOSITORY_URL"
git remote -v
git ls-remote origin

10-5. 원격 히스토리를 교체한다

검증한 로컬 브랜치와 태그를 원격에 반영하고 push가 동결되었다는 전제에서 다음 명령을 사용합니다.

# 고위험 작업: 공유 이력의 브랜치와 태그를 강제로 교체한다
git push --force origin --all
git push --force origin --tags

두 명령 역시 동료가 정리 도중 올린 변경을 잃게 할 수 있으며 보호 브랜치 정책 때문에 거부될 수 있습니다. 전체 refs/*를 정확히 확보한 mirror clone에서 민감 정보 제거처럼 모든 ref를 교체해야 하는 예외적인 작업만 호스팅 서비스의 공식 절차에 따라 git push --force --mirror origin을 검토합니다. --mirror는 원격 ref 삭제까지 반영하므로 일반 용량 정리의 기본 명령으로 사용하지 않습니다.

원격 반영 후에는 깨끗한 새 clone을 하나 더 만들어 최종 용량을 측정합니다. 로컬 정리 폴더의 숫자만으로 성공을 판단하면 reflog나 로컬 전용 ref 때문에 결과를 잘못 읽을 수 있습니다.

git clone "$REPOSITORY_URL" repository-verification
cd repository-verification
git fsck --full
git count-objects -vH

목차로 돌아가기

11. Windows·macOS·Linux에서 헷갈리는 부분

Git 명령 자체는 대부분 같지만 셸 명령은 다릅니다.

macOS·Linux·Git Bash에서는 du -sh .git으로 폴더 크기를 확인하고, 여러 줄 명령에 \를 사용하며, awk·sort·tail 같은 도구를 함께 쓸 수 있습니다.

Windows PowerShell은 ForEach-Object·Sort-Object·Select-Object를 사용합니다. CMD 명령인 rmdir /s /q와 PowerShell의 Remove-Item을 같은 예제처럼 섞어 쓰면 안 됩니다. 무엇보다 .git 폴더 자체를 지우는 방법은 특정 대용량 파일을 선별 정리하는 절차가 아니라 브랜치·태그·stash·설정을 포함한 버전 관리 정보를 통째로 포기하는 작업입니다. 이 글의 용량 정리 절차로 사용하지 않습니다.

Windows에서 git-filter-repo의 경로를 지정할 때는 Git 내부 경로처럼 /를 쓰는 편이 안전합니다.

git filter-repo --path 'assets/archive.zip' --invert-paths

운영체제별 긴 파이프라인을 유지보수하고 싶지 않다면 다음 두 명령을 공통 기준으로 삼는 것이 좋습니다.

git count-objects -vH
git filter-repo --analyze

목차로 돌아가기

12. 정리가 제대로 끝났는지 검증하기

성공 기준은 “현재 .git 숫자가 줄었다” 하나가 아닙니다. 필요한 이력을 보존하면서 새 clone에서도 감소가 재현되어야 합니다.

작업 전에 다음 항목을 메모하고, 작업 후 같은 기준으로 다시 기록합니다.

  • 전체 저장소와 .git 폴더 크기
  • git count-objects -vHsize-pack
  • 분석 보고서에서 확인한 가장 큰 blob
  • 브랜치와 태그 수
  • git fsck --full 결과
  • 애플리케이션 빌드와 테스트 결과
  • 원격에서 다시 받은 새 clone의 크기

다음 네 가지가 모두 충족되어야 정리가 끝났다고 볼 수 있습니다.

  1. git fsck --full에서 저장소 무결성 문제가 없다.
  2. 필요한 브랜치·태그·현재 파일이 유지되었다.
  3. 애플리케이션 빌드와 테스트가 통과한다.
  4. 원격에서 새로 clone한 저장소에서도 용량 감소가 재현된다.

호스팅 서비스 화면에 표시되는 저장소 크기는 서버의 가비지 컬렉션과 캐시 정책 때문에 즉시 바뀌지 않을 수 있습니다. 비밀 정보가 관련되었다면 일반 용량 감소와 달리 PR ref, fork, 캐시, 기존 clone까지 별도로 처리해야 합니다.

목차로 돌아가기

13. 자주 마주치는 실패와 해결 방법

git-filter-repo가 fresh clone이 아니라고 거부한다

데이터 손실을 막는 안전장치입니다. 현재 폴더에서 --force를 추가하기 전에 새 clone을 만들고, 로컬 저장소를 원본으로 clone했다면 --no-local을 사용합니다.

파일을 제거했는데 크기가 거의 줄지 않는다

같은 파일이 과거에 다른 이름으로 존재했거나, 다른 대용량 blob이 남았거나, 다른 브랜치·태그·특수 ref가 객체를 가리킬 수 있습니다. 필터링 전후에 git filter-repo --analyze를 다시 실행해 비교합니다.

정리 후 origin이 사라졌다

git-filter-repo의 안전 동작일 수 있습니다. 먼저 결과를 검증하고, 기록해 둔 정확한 URL을 git remote add origin으로 다시 등록합니다.

현재 버전의 필요한 파일까지 사라졌다

--invert-paths는 지정한 경로를 모든 히스토리에서 제거합니다. 원본 clone은 그대로 보관해야 하며, 필요한 경우 원본에서 현재 파일을 복원한 뒤 큰 파일 보관 정책을 Git LFS나 외부 아티팩트 저장소로 바꿔 새 커밋으로 추가합니다.

원격이 다시 커졌다

동료의 오래된 clone이 기존 히스토리를 다시 push했을 가능성이 있습니다. 히스토리 재작성 뒤 단순 git pull로 합치면 제거한 객체가 재유입될 수 있습니다. 모든 사용자가 작업을 중단하고 새 clone으로 전환해야 합니다.

deleted by us 충돌이 난다

커밋 시간 문제가 아니라 적용할 변경분의 기준 파일과 현재 이력이 맞지 않는 경우가 대부분입니다. 저장소 전체를 원격 상태로 맞출 목적이라면 새 clone이 더 단순합니다. 특정 기능만 옮겨야 한다면 대상 커밋의 부모와 선행 변경을 먼저 확인하고, 필요한 커밋 범위를 순서대로 가져옵니다.

로컬은 줄었는데 GitHub의 PR이나 캐시에서 파일이 보인다

일반 브랜치와 태그를 force push해도 호스팅 서비스의 읽기 전용 PR ref, fork, 캐시가 남을 수 있습니다. 비밀 정보 제거라면 자격 증명을 먼저 폐기하고 호스팅 서비스의 공식 지원 절차를 따라야 합니다.

목차로 돌아가기

14. 팀 저장소라면 추가로 해야 할 일

히스토리 재작성은 명령 한 번으로 끝나는 개인 작업이 아닙니다. 커밋 ID가 바뀌므로 이를 참조하는 모든 시스템이 영향을 받습니다.

작업 전

  • 정리 시간과 push 동결 시간을 공지합니다.
  • 열린 PR과 장기 작업 브랜치의 담당자를 확인합니다.
  • CI/CD, 배포 스크립트, 서브모듈, 릴리스 문서에 고정 SHA가 있는지 찾습니다.
  • 보호 브랜치와 태그 정책을 확인합니다.
  • 원격 ref 목록과 백업을 남깁니다.

작업 직후

  • 새 커밋 ID 기준으로 CI와 배포를 다시 검증합니다.
  • 팀원에게 기존 clone을 폐기하고 다시 clone하도록 안내합니다.
  • 꼭 보존해야 하는 미완성 작업은 오래된 clone에서 patch로 분리한 뒤 새 이력에 신중하게 적용합니다.
  • 오래된 브랜치나 태그를 그대로 push하지 않도록 안내합니다.
  • 서브모듈의 고정 커밋과 외부 자동화 설정을 갱신합니다.

기존 clone에서 무심코 pull한 다음 push하면 정리한 과거가 다시 들어올 수 있습니다. GitHub 공식 문서도 히스토리 재작성 뒤 다른 사용자의 오래된 clone이 재오염의 원인이 될 수 있다고 경고합니다. 팀 공지는 부가 작업이 아니라 정리 절차의 일부입니다.

목차로 돌아가기

15. 저장소가 다시 커지는 것을 막는 방법

한 번 정리한 뒤 같은 문제가 반복되지 않게 입력 단계에서 막습니다.

빌드 결과물과 로컬 데이터를 .gitignore에 추가한다

dist/
build/
*.log
*.zip
*.tar.gz
*.sql
*.dump

프로젝트 특성에 맞게 조정해야 합니다. SQL 마이그레이션 파일처럼 실제로 버전 관리해야 하는 파일까지 확장자만 보고 무조건 제외하면 안 됩니다.

큰 바이너리는 Git LFS나 아티팩트 저장소로 분리한다

git lfs install
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
git commit -m "chore: track large assets with Git LFS"

Git LFS는 저장소에 실제 대형 파일 대신 포인터를 저장합니다. 다만 지금 LFS를 켠다고 이미 일반 Git 히스토리에 들어간 과거 blob이 자동으로 줄지는 않습니다. 기존 이력은 별도의 마이그레이션 또는 필터링이 필요합니다.

커밋 전에 크기를 검사한다

pre-commit hook이나 CI에서 일정 크기를 넘는 파일을 거부하면 실수를 원격에 올리기 전에 발견할 수 있습니다. 배포 ZIP, 동영상, 데이터베이스 덤프는 Git 릴리스 이력보다 아티팩트 저장소나 오브젝트 스토리지에 보관하는 편이 적합합니다.

주기적으로 측정한다

git count-objects -vH
git filter-repo --analyze

갑자기 커진 시점을 일찍 발견하면 공유된 히스토리 전체를 다시 쓰지 않고 최근 커밋만 수정할 수 있습니다. 저장소가 느려진 뒤 정리하는 것보다 증가 추세를 확인하는 편이 훨씬 안전합니다.

목차로 돌아가기

16. 최종 체크리스트

  • [ ] 원격 연결 제거와 객체 삭제를 구분했는가?
  • [ ] 작업 트리가 아니라 .git이 실제 원인인지 측정했는가?
  • [ ] 미커밋·미푸시 작업과 원격 URL을 기록했는가?
  • [ ] 원본 폴더 또는 검증 가능한 백업을 보관했는가?
  • [ ] 일반 재클론으로 해결 가능한 로컬 문제인지 먼저 확인했는가?
  • [ ] shallow·partial·sparse clone의 차이를 이해했는가?
  • [ ] 삭제할 파일의 과거 경로와 이름 변경 이력을 확인했는가?
  • [ ] 히스토리 재작성 전에 팀 push를 동결했는가?
  • [ ] --force--mirror의 영향을 이해했는가?
  • [ ] 정리 후 git fsck, 빌드, 테스트를 통과했는가?
  • [ ] 원격에서 새로 clone해 용량 감소를 재확인했는가?
  • [ ] 기존 clone이 과거 이력을 다시 push하지 않도록 안내했는가?
  • [ ] .gitignore, Git LFS, 파일 크기 검사 정책을 추가했는가?

마무리

git remote remove가 실패한 것은 아닙니다. 애초에 용량을 정리하는 명령이 아니었습니다. 원격 주소가 사라지는 것과 로컬 객체가 없어지는 것은 별개의 일이고, 현재 파일을 삭제하는 것과 모든 과거 커밋에서 그 파일을 지우는 것 역시 다릅니다.

로컬 공간만 확보하면 된다면 새 clone, shallow clone, partial clone부터 검토하는 것이 안전합니다. 도달 불가능한 로컬 객체만 문제라면 참조와 reflog를 확인한 뒤 GC를 사용할 수 있습니다. 반면 새 clone도 계속 크고 모든 사용자에게서 특정 대용량 파일을 없애야 한다면, 그때 git-filter-repo로 히스토리를 재작성합니다.

중요한 것은 가장 강한 명령을 빨리 실행하는 것이 아니라 줄이려는 대상이 연결 정보인지, 로컬 찌꺼기인지, 원격의 공유 히스토리인지 먼저 구분하는 것입니다. 이 구분만 정확하면 불필요한 충돌과 복구 불가능한 삭제를 피하면서 저장소를 제대로 가볍게 만들 수 있습니다.

참고 자료

태그

mLog

8년 이상 풀스택 개발자로 일하고 있습니다. Spring Boot, PostgreSQL, Redis, Vue·TypeScript와 Docker·Linux 서버를 다루며, 직접 운영하고 해결한 내용을 공식 문서와 실행 결과를 바탕으로 정리합니다.