LabHub
배우기 러닝패스 코스

Git 실전 · 토큰이 이력에 들어갔다 · 이론

지웠다고 사라진 것이 아니다

LabHub 에서 이어서 보기

한 줄 요약

파일을 지우고 커밋하는 것은 앞으로 그 파일이 없다 는 뜻일 뿐, 이력에 남은 객체는
그대로입니다. 실제로 없애려면 이력을 재작성하고(filter-branch), 백업 참조를 걷어내고,
reflog 를 비우고, gc --prune=now 까지 해야 합니다. 그래도 이미 퍼진 사본은 남습니다.

왜 이게 필요했나

두 가지 사고가 같은 모양입니다.

하나는 비밀입니다. 운영 토큰이 든 .env 를 실수로 커밋했다가 알아채고 지웠습니다.
저장소를 열어 보면 그 파일은 없습니다. 그런데 git log -- conf/prod.env 는 여전히
커밋 두 개를 내고, git cat-file -p <커밋>:conf/prod.env 는 토큰 원문을 그대로
찍어 줍니다. git 은 내용 주소 저장소라, 한 번 들어온 blob 은 그것을 가리키는 커밋이
남아 있는 한 사라지지 않습니다.

다른 하나는 크기입니다. 수백 MB짜리 데이터 파일이나 빌드 산출물을 한 번 커밋하면,
그 뒤에 지워도 clone 받는 사람마다 그 객체를 계속 내려받습니다. "왜 우리 저장소는
clone 에 10분이 걸리지" 의 답이 대개 여기 있습니다.

둘 다 앞으로 가 아니라 지난 이력을 고쳐야 풀립니다.

어떻게 동작하나

git filter-branch 는 지정한 ref 들의 커밋을 하나씩 다시 만들면서, 각 커밋마다 필터를
적용합니다. 파일을 들어낼 때는 인덱스만 고치는 --index-filter 가 가장 빠릅니다 —
작업 트리를 펼치지 않기 때문입니다.

FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --index-filter \  'git rm -r --cached --ignore-unmatch conf/prod.env assets/blob.bin' \  --prune-empty --tag-name-filter cat -- --all

옵션 하나하나가 이유를 갖습니다.

| 옵션 | 왜 필요한가 |
| --- | --- |
| --ignore-unmatch | 그 파일이 없는 커밋에서 git rm 이 실패해 전체가 멈추지 않게 |
| --prune-empty | 그 파일만 추가하던 커밋이 빈 커밋으로 남지 않게 |
| --tag-name-filter cat | 태그가 옛 커밋을 가리킨 채 남지 않게 |
| -- --all | 브랜치 하나가 아니라 모든 ref 를 재작성 |
| FILTER_BRANCH_SQUELCH_WARNING | git 이 띄우는 10초 경고를 넘기기 위해 |

여기서 대부분이 멈춥니다. 명령이 끝나고 git log 를 보면 파일이 없는데,
git rev-list --objects --all 에는 그 파일이 여전히 나옵니다. 이유는
refs/original/ 입니다. filter-branch 는 되돌릴 수 있도록 원래 ref 를 전부 그 아래
백업해 두는데, 그것도 ref 라서 옛 커밋과 옛 blob 을 붙잡고 있습니다.

그래서 순서가 있습니다.

1. refs/original/ 을 지운다 — git for-each-ref --format='%(refname)' refs/original
로 목록을 뽑아 git update-ref -d 를 돌린다.
2. reflog 를 비운다 — git reflog expire --expire=now --expire-unreachable=now --all.
reflog 도 참조다. HEAD 가 지나간 자리를 전부 기억한다.
3. git gc --prune=now 로 도달 불가 객체를 지운다. --prune=now 가 없으면 기본
유예 기간(2주)이 지나야 지워진다.

git count-objects -vH 로 전후를 재면 이 과정이 실제로 무엇을 했는지 숫자로 보입니다.

현장에서 만나는 모습

재작성이 끝나면 모든 커밋 해시가 바뀝니다. 커밋 해시는 부모와 트리를 포함해
계산되므로, 이력 맨 앞에서 무엇을 하나 고치면 그 뒤가 전부 달라집니다. 그래서 다음
일들이 따라옵니다.

그리고 가장 중요한 것 — 이미 clone 을 떠 간 사본에는 그 객체가 그대로 남습니다.
동료의 노트북, CI 캐시, 포크한 저장소, 백업에 모두 남아 있습니다. 그래서 비밀이
이력에 들어갔을 때 첫 번째로 할 일은 이력 청소가 아니라 **그 비밀을 폐기하고 새로
발급하는 것**입니다. 이력 청소는 두 번째입니다. 순서를 바꾸면 청소하는 며칠 동안
살아 있는 자격 증명이 밖에 있게 됩니다.

큰 저장소에서는 git filter-branch 가 느립니다. 공식 문서도 git filter-repo
BFG 같은 도구를 권합니다. 다만 그 도구들은 따로 설치해야 하고, 하는 일과 뒤처리
순서는 똑같습니다 — 재작성, 백업 참조 제거, reflog 만료, gc.

다음 실습에서 할 것

토큰이 든 설정 파일과 900KB짜리 덩어리가 섞인 이력을 만들고, 워킹 트리에서 지워도
git cat-file 로 원문이 나오는 것부터 확인합니다. 그다음 재작성을 돌리고, 아직
남아 있는 이유(refs/original)를 직접 찾아 걷어내고, 크기가 실제로 줄어드는 것을
git count-objects -vH 로 잽니다. 마지막으로 미리 떠 둔 동료의 사본에 그 비밀이
그대로 살아 있는 것을 보여 주고, 그래서 무엇을 먼저 해야 하는지로 끝냅니다.

공식 문서는 [git-filter-branch](https://git-scm.com/docs/git-filter-branch),
[git-reflog](https://git-scm.com/docs/git-reflog), [git-gc](https://git-scm.com/docs/git-gc) 입니다.