클론이 몇 분씩 걸리거나 .git이 소스보다 몇 배 크거나, 지운 브랜치의 커밋이 다시 필요해지면 어디부터 손대야 할지 막막해요. 결론부터 말하면 CI처럼 한 번 쓰고 버리는 곳은 --depth=1, 사람이 쓰는 개발 PC는 --filter=blob:none, 자주 바뀌는 큰 바이너리는 처음부터 Git LFS가 기본값이에요. gc와 reflog 설정은 대부분 기본값이 맞고, 지운 커밋은 기본 설정에서 대략 30일 안에 git reflog로 찾으면 돼요.
이 글은 객체 구조보다 운영 판단을 다뤄요. 판단표, 깃 2.52.0으로 직접 잰 숫자, 짧은 해시 충돌 계산, 증상별 확인 순서, SHA-256·reftable 도입 전 점검을 차례로 정리했어요.
깃 저장소 설정 판단표 — 무엇을 언제 바꾸나
깃 설정은 수백 개지만, 저장소 크기와 속도에 영향을 주는 결정은 몇 개로 좁혀져요. 아래 표는 git-clone 문서, git-gc 문서, git-init 문서에 적힌 기본값과 동작을 상황별로 묶은 거예요.
| 결정 | 기본값 | 바꿀 때 | 그대로 둘 때 |
|---|---|---|---|
| 클론 범위 | 전체 기록 | CI는 --depth=1, 큰 저장소 개발 PC는 --filter=blob:none | 저장소가 수백 MB 이하, 오프라인 작업이 잦음 |
| 큰 바이너리 | 그냥 커밋 | 자주 바뀌는 디자인 원본·모델 파일·빌드 산출물 → Git LFS 또는 릴리스 첨부 | 거의 안 바뀌는 작은 아이콘·폰트 |
| 자동 정리 | gc.auto=6700, gc.autoPackLimit=50 | 대형 저장소는 git maintenance start로 백그라운드 정리 | 일반 저장소 |
| reflog 보관 | 닿는 항목 90일, 닿지 않는 항목 30일 | 실수 복구 기간을 늘리고 싶으면 gc.reflogExpireUnreachable 연장 | 대부분 |
| 해시 형식 | sha1 | 새 저장소이고 호스팅·도구가 전부 SHA-256 지원 | 기존 저장소, SHA-256을 못 읽는 도구가 하나라도 있음 |
| ref 저장 형식 | files(낱개 파일 + packed-refs) | 브랜치·태그가 수만 개, 대소문자만 다른 브랜치 충돌이 잦음 | 다른 깃 구현(JGit·libgit2 기반 도구)을 쓰는 곳 |
기본값을 바꾸는 설정은 대부분 규모가 커졌을 때 쓰는 것이라 작은 저장소에서는 얻는 게 거의 없어요. 반대로 바이너리 정책과 해시 형식은 나중에 바꾸기가 가장 비싸서 저장소를 만들 때 정해요.
클론 방식 고르기 — 전체·blobless·얕은 클론 크기 실측
커밋 50개짜리 예제 저장소에서 재 봤어요. 커밋마다 1 MB 무작위 바이너리(asset.bin)를 새로 쓰고 텍스트에 20 KB씩 덧붙였고, uploadpack.allowFilter=true를 켠 뒤 file://로 클론했어요.
세 방식이 비슷한 건 이 예제의 무게가 옛 파일 내용에 몰려 있어서예요. 커밋이 수십만 개면 tree까지 받는 blobless와 안 받는 treeless의 차이가 벌어져요.
가볍다고 공짜는 아니에요. blobless 클론에서 git log -p -- asset.bin을 돌리자 4.5초가 걸렸고 팩이 48.2 MiB로 늘었어요. 옛 버전 50개를 그 자리에서 받아 온 거예요. partial-clone 설계 문서도 없는 객체를 그때그때 받아 오는 동작은 객체를 하나씩 가져와서 느린 편이라고 적어요. 실제 네트워크라면 더 느려져요.
얕은 클론은 기록이 잘려 있다는 점을 기억해야 해요. git-clone 문서 기준으로 --depth는 --single-branch를 함께 뜻하므로 다른 브랜치가 안 보여요. 그래서 CI에서 "main과 비교해 바뀐 파일만 검사" 같은 작업을 하면 merge-base를 못 찾아 실패해요. 이때는 --depth를 늘리기보다 그 작업만 blobless 클론으로 바꾸는 편이 깔끔해요.
바이너리가 저장소를 키우는 속도 — 직접 계산
깃은 바뀐 파일을 통째로 새 객체로 저장하고, git gc가 비슷한 객체끼리 차이만 남겨 묶어요. 이 묶음이 잘 되는지 두 저장소로 비교했어요.
- 텍스트: 약 6 MB 텍스트 파일, 커밋마다 무작위 50줄 수정, 11개 버전
- 바이너리: 5 MB 무작위 바이트 파일(압축된 zip·png·mp4와 비슷한 성질), 커밋마다 새로 생성, 11개 버전
이 결과를 공식으로 쓰면 바이너리 저장소의 증가량은 대략 파일 크기 × 바뀐 횟수예요. 20 MB짜리 디자인 원본을 하루 3번씩 1년(약 250 근무일) 고치면 20 × 3 × 250 = 15,000 MB, 약 15 GB가 기록에 쌓여요. GitHub의 대용량 파일 안내는 저장소를 1 GB 미만, 적어도 5 GB 미만으로 두라고 권하고, 50 MiB가 넘는 파일은 경고, 100 MiB가 넘는 파일은 푸시를 막아요. MiB와 MB가 섞여 헷갈리면 데이터 용량 변환기로 1024 기준 값을 맞춰 보세요.
git-config 문서 기준으로 core.bigFileThreshold(기본 512 MiB)보다 큰 파일은 차이 저장(delta)을 아예 시도하지 않아요. 위 실험처럼 그보다 작아도 압축된 형식이면 차이 저장이 거의 안 돼요.
.git이 커졌을 때 무엇이 범인인지는 이 한 줄로 찾아요.
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectsize) %(objectsize:disk) %(rest)' \
| awk '$1=="blob"' | sort -k3 -n -r | head -10
세 번째 칸(objectsize:disk)이 실제 디스크 크기예요. 예제에서 1,000,000바이트짜리 asset.bin은 디스크 1,000,320바이트로 안 줄었고, 같은 크기의 docs/notes.txt는 451,672바이트였어요. 안 줄어드는 큰 파일이 LFS 후보예요.
브랜치 1만 개의 비용 — 낱개 ref·packed-refs·reftable 실측
브랜치를 만드는 봇이나 빌드마다 붙는 태그가 쌓이면 ref가 수만 개가 돼요. 브랜치 10,000개를 만들어 세 저장 방식을 비교했어요(다섯 번 돌린 가운데 값).
| 저장 방식 | 디스크 사용 | for-each-ref | branch --list | rev-parse b5000 |
|---|---|---|---|---|
| 낱개 파일 (10,001개) | 약 39 MiB (40,004 KB) | 1,287 ms | 379 ms | 9.7 ms |
git pack-refs --all 뒤 packed-refs | 578,997 B | 660 ms | 111 ms | 15.8 ms |
git refs migrate --ref-format=reftable 뒤 | 335,338 B | 644 ms | 109 ms | 15.5 ms |
낱개 파일이 39 MiB인 이유는 내용(41바이트)이 아니라 파일 시스템의 최소 할당 단위예요. 파일 하나가 4 KiB를 차지해 10,001 × 4 KiB ≈ 39 MiB가 됐어요. 느슨한 객체도 같아서, 자동 gc 기준인 6,700개는 최소 26 MiB예요.
목록 조회는 묶은 쪽이 2~3배 빨랐고, 이름 하나를 찾는 rev-parse는 낱개 파일이 몇 밀리초 빨랐어요. git gc가 packed-refs 묶기를 같이 하므로 자동 정리를 켜 두면 따로 할 일은 없어요.
reftable이 확실히 나은 지점은 크기보다 이름 충돌이에요.
- 대소문자 충돌: 기본 형식에서
feature가 있을 때git branch Feature는a branch named 'Feature' already exists로 실패했어요(macOS 기본 파일 시스템). reftable에서는 둘 다 만들어졌어요. BreakingChanges 문서가 reftable로 바꾸려는 첫 이유로 이 문제를 꼽아요. - 폴더·파일 충돌:
feature가 있을 때feature/login은 두 형식 모두'refs/heads/feature' exists; cannot create로 실패했어요. ref 이름 규칙이라 reftable로도 안 풀려요. 이름을feature/이름으로 통일해 두면 생기지 않아요.
짧은 해시는 언제 겹치나 — 충돌 확률 계산
git log --oneline이 보여 주는 7자리 해시는 16진수 7자, 즉 28비트예요. 객체가 n개일 때 앞 28비트가 같은 쌍이 하나라도 있을 확률은 생일 문제 공식 1 − e^(−n(n−1)/2^29)로 어림해요. 직접 계산하면 이래요.
객체 3만 개면 7자리가 겹치는 쌍이 있을 확률이 80%를 넘어요. 커밋·tree·blob을 모두 센 수예요.
표로 보기
| 구분 | 7자리 (28비트) | 8자리 (32비트) |
|---|---|---|
| 1천 | 0.19 | 0.01 |
| 5천 | 4.5 | 0.29 |
| 1만 | 17 | 1.2 |
| 3만 | 81.3 | 9.9 |
| 10만 | 100 | 68.8 |
git-config 문서의 core.abbrev 설명대로, 깃은 따로 정하지 않으면 팩에 든 객체 수에 맞춰 짧은 해시 길이를 자동으로 늘려요. 객체 약 22만 개(git count-objects -v의 in-pack 222,692)인 저장소에서 git log의 짧은 해시는 9자리였어요. 문제는 지금 고유했던 짧은 해시가 나중에도 고유하다는 보장이 없다는 점이에요. 저장소가 커지면 short object ID ... is ambiguous 오류가 나요.
그래서 실무 규칙은 이래요. 사람이 읽는 화면에는 짧은 해시를 써도 되지만, 배포 기록·CI 아티팩트 이름·이슈 트래커 연동처럼 오래 남는 곳에는 40자 전체 해시(git rev-parse HEAD)를 저장해요. 참고로 깃 해시는 파일 내용만의 해시가 아니라 앞에 blob 크기 헤더를 붙인 값의 해시라서, 해시 생성기로 파일 내용을 해시한 값과 다르게 나와요. 해시 함수 자체의 차이는 MD5 SHA-1 SHA-256 차이 글에 정리해 뒀어요.
지운 커밋 되살리기 — reflog·gc 기본값과 기한
브랜치를 지우거나 reset --hard를 해도 객체는 바로 사라지지 않아요. 실험 저장소에서 tmp 브랜치에 커밋하고 git branch -D tmp로 지운 뒤에도 git reflog에 HEAD@{1}: commit: lost 줄이 남아 있었어요. 브랜치의 reflog는 함께 지워져도 HEAD의 reflog는 남기 때문이에요.
기한은 git-gc 문서의 세 기본값이 정해요.
| 설정 | 기본값 | 뜻 |
|---|---|---|
gc.reflogExpire | 90일 | 현재 브랜치 끝에서 닿는 reflog 항목 보관 기간 |
gc.reflogExpireUnreachable | 30일 | 현재 끝에서 닿지 않는 항목(지운 커밋이 여기) 보관 기간 |
gc.pruneExpire | 2주 | 아무도 가리키지 않는 객체를 지우기 전 유예 기간 |
2주 유예는 닿지 않게 된 날이 아니라 객체 파일의 나이로 따져요. 만든 지 한 달 된 커밋은 reflog 항목이 만료된 뒤 다음 gc에서 바로 지워질 수 있어서, 30일 + 2주가 아니라 약 30일로 잡아요. 반대로 gc가 안 돈 저장소에는 더 오래된 객체가 남아 있기도 해요.
복구 순서는 이래요.
git reflog(또는git reflog show 브랜치이름)에서 커밋 메시지로 해시를 찾아요.git branch rescue 해시로 이름을 붙여요. 이름이 붙는 순간 정리 대상에서 빠져요.- reflog에 없으면
git fsck --unreachable --no-reflogs나git fsck --lost-found로 남은 객체를 훑어요. - 찾기 전에는
git gc --prune=now를 돌리지 않아요. 문서도 동시에 쓰는 프로세스가 있으면 저장소가 망가질 위험이 커진다고 적어요.
reflog는 로컬 기록이라 서버나 동료 클론에는 없어요. 푸시하지 않은 커밋을 잃었다면 그 컴퓨터에서 찾아야 해요.
운영에서 자주 나는 문제와 확인 순서
증상별로 처음 볼 곳이에요.
| 증상 | 첫 확인 | 다음 확인 | 흔한 해결 |
|---|---|---|---|
| 클론이 너무 느림 | git count-objects -vH의 size-pack | 위 큰 blob 찾기 명령 | CI는 --depth=1, PC는 --filter=blob:none, 큰 파일은 LFS |
cannot lock ref ... exists; cannot create | git for-each-ref refs/heads/이름 | 같은 앞부분 이름의 브랜치 | 이름 규칙 통일, 오래된 원격 추적 ref는 git fetch --prune |
a branch named 'X' already exists (대소문자만 다름) | macOS·Windows 파일 시스템인지 | 원격에 대소문자만 다른 두 브랜치 | 원격에서 하나로 정리, 새 저장소면 reftable 검토 |
| 느슨한 객체 경고·명령마다 gc가 돎 | git count-objects -v의 count | gc.auto 값 | git maintenance start로 백그라운드로 옮겨요 |
bad object·missing blob | git fsck --full | blobless 클론인지(remote.origin.promisor) | blobless면 네트워크 확인 후 다시 시도, 아니면 새로 클론 |
느슨한 객체 경고는 숫자로 감을 잡을 수 있어요. 파일 5개를 고치는 커밋은 blob 5개, 바뀐 폴더마다 tree 하나(최상위 포함 보통 2~4개), commit 1개를 만들어 10개 안팎이에요. 6,700 ÷ 10 ≈ 670커밋마다 자동 gc가 한 번 도는 셈이에요. fetch로 받은 객체는 팩으로 와서 이 숫자에 거의 안 들어가요.
git-maintenance 문서 기준으로 git maintenance start는 매시간 스케줄을 걸고, incremental 전략으로 무거운 gc는 끄고 commit-graph·prefetch는 매시간, 느슨한 객체 정리·증분 재압축은 매일 돌려요. 명령 중간의 자동 정리(maintenance.auto)도 꺼져서 커밋 뒤 멈칫하는 일이 사라져요.
SHA-256·reftable 도입 전에 따질 것
BreakingChanges 문서에 따르면 깃 3.0에서 새 저장소의 기본 해시는 sha256, 기본 ref 형식은 reftable로 바뀔 예정이에요. 문서는 출시 날짜가 아직 없다고 적고, 두 변경 모두 생태계(라이브러리·애플리케이션·호스팅)가 준비되는 것을 전제 조건으로 걸어요. 지금 직접 켜기 전에 확인할 것은 이래요.
SHA-256
- git-init 문서는 현재 SHA-256 저장소와 SHA-1 저장소 사이에 상호운용이 없다고 적어요. 기존 저장소를 그 자리에서 바꾸는 방법도, SHA-1 원격과 주고받는 방법도 없다는 뜻이에요.
- 확인 순서는 호스팅(푸시·PR·웹 화면) → CI 러너의 깃 버전 → IDE·배포 도구의 깃 라이브러리예요.
- hash-function-transition 문서에 따르면 2017년 SHAttered 충돌 시연 뒤 깃 2.13.0부터 그 공격을 막는 강화 SHA-1을 기본으로 써요. 문서는 SHA-1이 여전히 약하다고 적지만, 인증 요건이 없다면 기존 저장소를 옮길 만큼 급하지는 않아요.
reftable
- 기존 저장소는 깃 2.52.0의
git refs migrate --ref-format=reftable로 옮길 수 있어요. git-refs 문서가 적은 한계는 워크트리가 있는 저장소는 옮길 수 없다는 것, 옮기는 동안 동시 쓰기를 막을 방법이 없다는 것이에요. 예약된 maintenance가 있으면 먼저 해제하라고 권해요. - BreakingChanges 문서는 JGit·libgit2·Gitoxide 같은 다른 구현의 지원을 전제로 꼽아요.
.git/refs를 직접 읽는 IDE·CI 플러그인은 옮긴 뒤 브랜치를 못 찾을 수 있어요. - 이득이 큰 곳은 대소문자 충돌이 잦은 macOS·Windows 혼합 팀, ref가 수만 개인 저장소예요. 브랜치 수십 개라면 packed-refs로 충분해요.
자주 묻는 질문
blobless 클론과 얕은 클론 중 개발 PC에는 무엇이 좋나요?
blobless 클론(--filter=blob:none)이 좋아요. 커밋과 tree를 모두 받아 log·merge-base·브랜치 비교가 정상으로 되고, 옛 파일 내용만 필요할 때 받아 와요. 얕은 클론은 기록이 잘리고 다른 브랜치도 없어 자주 막혀요.
git gc를 주기적으로 직접 돌려야 하나요?
대부분 필요 없어요. 느슨한 객체 6,700개나 팩 50개를 넘으면 커밋 같은 명령이 git gc --auto를 스스로 돌려요. 그 순간 멈칫하는 게 거슬리면 git maintenance start로 백그라운드에 맡겨요.
실수로 지운 브랜치는 언제까지 살릴 수 있나요?
기본 설정이면 약 30일 안에 git reflog로 해시를 찾아 git branch 새이름 해시로 되살리면 돼요. reflog는 로컬에만 있어서 그 커밋을 만든 컴퓨터에서 찾아야 하고, git gc --prune=now를 돌린 뒤에는 되살릴 수 없어요.
새 저장소를 지금 SHA-256으로 만들어도 되나요?
쓰는 호스팅과 모든 도구가 SHA-256 저장소를 지원할 때만 권해요. git-init 문서 기준으로 SHA-1 저장소와 상호운용이 없어서, 나중에 지원하지 않는 서비스와 연동해야 하면 되돌리기 어려워요. 깃 3.0에서 기본값이 될 예정이지만 날짜는 정해지지 않았어요.
정리
- 클론은 CI
--depth=1, 개발 PC--filter=blob:none, 오프라인·기록 분석이 잦으면 전체 클론이에요. - 자주 바뀌는 압축 바이너리는 크기 × 바뀐 횟수만큼 그대로 쌓여요. 첫 커밋 전에 LFS나 릴리스 첨부로 정해요.
- 브랜치 수만 개는 packed-refs로 충분히 버티고, 대소문자 충돌이 문제라면 reftable이 답이에요.
feature와feature/x충돌은 어떤 형식으로도 안 풀려요. - 오래 남는 기록에는 전체 해시를 저장하고, 지운 커밋은 30일 안에 reflog로 찾아요.