> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# Git remote를 지웠는데 용량이 그대로인 이유: .git 대용량 파일 진단과 안전한 정리
- URL: https://mlog.me/git-remote-remove-repository-size-cleanup/
- Published: 2026-09-03T12:15:18.000Z
- Updated: 2026-09-03T12:15:18.000Z
- Description: git remote remove를 실행해도 .git 폴더는 자동으로 줄지 않습니다. 원격 연결과 로컬 객체의 차이부터 대용량 blob 탐색, 재클론, partial clone, git-filter-repo를 이용한 히스토리 정리까지 안전한 순서로 정리합니다.
- Author: mLog
- Tags: 개발, Git·GitLab, 저장소 관리, #Git

더 이상 사용하지 않는 원격 저장소를 분리하려고 `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를 지웠는데 왜 용량은 그대로일까](#remote-is-not-storage)
2. [Git이 데이터를 보관하는 방식을 먼저 이해하자](#git-object-model)
3. [처음 시도한 방법들이 해결책이 아니었던 이유](#failed-attempts)
4. [삭제 전에 반드시 확인할 것](#preflight-check)
5. [실제 용량을 차지하는 대상을 찾는 방법](#find-large-objects)
6. [목적에 맞는 해결 방법 선택하기](#decision-guide)
7. [가장 안전한 로컬 정리: 새로 클론하기](#reclone)
8. [shallow·partial·sparse clone의 차이](#clone-strategies)
9. [참조가 끊긴 로컬 객체만 정리하기](#local-gc)
10. [대용량 파일을 히스토리에서 완전히 삭제하기](#filter-repo)
11. [Windows·macOS·Linux에서 헷갈리는 부분](#os-differences)
12. [정리가 제대로 끝났는지 검증하기](#verification)
13. [자주 마주치는 실패와 해결 방법](#troubleshooting)
14. [팀 저장소라면 추가로 해야 할 일](#team-migration)
15. [저장소가 다시 커지는 것을 막는 방법](#prevention)
16. [최종 체크리스트](#final-checklist)

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

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

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

```bash
git remote remove origin

```

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

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

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

```bash
git remote prune origin

```

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

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

[목차로 돌아가기](#toc)

## 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와 유예 기간이 끝난 후 가비지 컬렉션 대상이 될 수 있습니다. 이 둘을 구분해야 합니다.

[목차로 돌아가기](#toc)

## 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`을 함께 사용해야 합니다. 작업 트리 축소와 객체 전송 축소는 별개의 기능입니다.

[목차로 돌아가기](#toc)

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

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

```bash
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을 만들 수 있습니다.

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

```

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

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

[목차로 돌아가기](#toc)

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

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

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

```bash
git count-objects -vH

```

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

```bash
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`가 설치되어 있다면 다음 명령은 저장소를 변경하지 않고 분석 보고서를 만듭니다.

```bash
git filter-repo --analyze

```

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

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

```bash
# macOS
brew install git-filter-repo

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

```

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

```bash
git filter-repo --version

```

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

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

```bash
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 객체로 변환합니다.

```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`를 공통 진단법으로 쓰는 편이 가장 덜 깨집니다.

[목차로 돌아가기](#toc)

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

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

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

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

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

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

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

[목차로 돌아가기](#toc)

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

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

```bash
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으로 복원되지 않을 수 있습니다.

[목차로 돌아가기](#toc)

## 8\. shallow·partial·sparse clone의 차이

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

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

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

```bash
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

```bash
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

```bash
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`만 켰을 때보다 “다운로드 용량을 줄인다”는 목적에 더 정확합니다.

[목차로 돌아가기](#toc)

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

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

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

```bash
git gc
git count-objects -vH

```

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

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

```

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

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

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

[목차로 돌아가기](#toc)

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

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

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

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

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

```bash
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`을 사용합니다.

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

```

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

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

```bash
git filter-repo --analyze

```

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

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

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

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

```

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

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

```

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

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

```

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

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

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

```bash
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을 다시 등록합니다.

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

```

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

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

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

```

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

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

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

```

[목차로 돌아가기](#toc)

## 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 내부 경로처럼 `/`를 쓰는 편이 안전합니다.

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

```

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

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

```

[목차로 돌아가기](#toc)

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

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

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

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

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

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

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

[목차로 돌아가기](#toc)

## 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, 캐시가 남을 수 있습니다. 비밀 정보 제거라면 자격 증명을 먼저 폐기하고 호스팅 서비스의 공식 지원 절차를 따라야 합니다.

[목차로 돌아가기](#toc)

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

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

### 작업 전

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

### 작업 직후

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

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

[목차로 돌아가기](#toc)

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

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

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

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

```

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

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

```bash
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 릴리스 이력보다 아티팩트 저장소나 오브젝트 스토리지에 보관하는 편이 적합합니다.

### 주기적으로 측정한다

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

```

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

[목차로 돌아가기](#toc)

## 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`로 히스토리를 재작성합니다.

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

## 참고 자료

- [Git remote 공식 문서](https://git-scm.com/docs/git-remote?ref=mlog.me)
- [Git 객체 용량 확인 공식 문서](https://git-scm.com/docs/git-count-objects?ref=mlog.me)
- [Git rev-list 공식 문서](https://git-scm.com/docs/git-rev-list?ref=mlog.me)
- [Git garbage collection 공식 문서](https://git-scm.com/docs/git-gc?ref=mlog.me)
- [Git clone의 shallow·partial 옵션 공식 문서](https://git-scm.com/docs/git-clone?ref=mlog.me)
- [Git partial clone 공식 문서](https://git-scm.com/docs/partial-clone?ref=mlog.me)
- [Git sparse-checkout 공식 문서](https://git-scm.com/docs/git-sparse-checkout?ref=mlog.me)
- [git-filter-branch의 공식 경고](https://git-scm.com/docs/git-filter-branch?ref=mlog.me)
- [git-filter-repo 공식 문서](https://github.com/newren/git-filter-repo/blob/main/Documentation/git-filter-repo.txt?ref=mlog.me)
- [git-filter-repo 설치 안내](https://github.com/newren/git-filter-repo/blob/main/INSTALL.md?ref=mlog.me)
- [GitHub의 히스토리 재작성 및 민감 정보 제거 안내](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository?ref=mlog.me)
- [Git bundle 공식 문서](https://git-scm.com/docs/git-bundle?ref=mlog.me)
- [GitHub의 Git LFS 안내](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-git-large-file-storage?ref=mlog.me)