Git LFS는 파일을 어디에 저장할까 — 포인터와 실제 파일의 관계
Git LFS가 Git 이력과 실제 파일을 분리하는 구조를 이해하고, PPTX·PNG 적용 기준부터 clone·push 동작, 비용과 백업의 경계까지 살펴본다.
문서는 Markdown으로 작성하더라도 발표 자료는 PPTX, 화면 캡처는 PNG로 남는 경우가 많다. 이 파일들을 소스와 함께 버전 관리하려고 Git LFS를 적용하면 질문이 생긴다. 같은 저장소에 커밋하는데, 실제 파일은 어디에 저장되는 걸까?
Git LFS(Large File Storage)는 Git 이력에는 작은 포인터 파일을 기록하고, 실제 파일 내용은 LFS 저장 공간에서 관리한다. 새 Git 저장소를 하나 더 만드는 것이 아니라, 하나의 파일 버전을 두 저장 경로로 나누어 관리하는 구조다.
이 차이를 이해하면 LFS를 언제 써야 하는지뿐 아니라, 왜 Git 저장소만 백업해서는 부족할 수 있는지도 설명할 수 있다.
1. 같은 파일 경로에 두 종류의 내용이 있다
LFS로 관리하는 slides.pptx도 작업 폴더에서는 평소처럼 PowerPoint로 열 수 있다. 그러나 커밋에 기록된 같은 경로의 내용은 PPTX 바이너리가 아니라 포인터다.
1
2
3
4
5
6
7
작업 폴더: slides.pptx (실제 파일)
│ git add 시 분리
├─ 포인터 → Git Index → Git 커밋 ── push ──→ 원격 Git 저장소
│ │
│ oid로 식별
│ ▼
└─ 실제 내용 → 로컬 LFS 캐시 ── 업로드 ──→ 원격 LFS 저장 공간
위 그림에서 Git 커밋의 push 대상은 Git 저장소이고, 실제 파일의 업로드 대상은 LFS 저장 공간이다. 둘은 같은 서비스에서 제공할 수 있지만 별도의 데이터 전송 경로다.
| 위치 | 보관하는 것 | 역할 |
|---|---|---|
| 작업 폴더 | 현재 버전의 실제 파일 | 편집·빌드·열람 |
| Git Index·커밋의 blob | 포인터 텍스트 | 해당 경로가 어떤 파일 내용을 참조하는지 기록 |
| 로컬 LFS 캐시 | 내려받거나 추가한 실제 파일 내용 | 재사용 및 원격 전송 준비 |
| 원격 LFS 저장 공간 | 업로드한 실제 파일 내용 | 다른 작업 사본에서 복원할 수 있게 제공 |
일반적인 로컬 저장소에서 LFS 캐시는 .git/lfs/objects 아래에 있다. 그래서 LFS를 쓴다고 내 컴퓨터의 .git 디렉터리 전체가 항상 작아지는 것은 아니다. Git 객체 이력에는 포인터를 넣지만, 내려받은 실제 파일은 별도 로컬 캐시에 남을 수 있다.
Git의 blob·tree·commit 관계는 Git 내부 모델에서 설명한다. LFS 포인터도 Git 관점에서는 평범한 blob이라는 점이 연결 지점이다.
2. 포인터는 다운로드 URL이 아니다
LFS 포인터의 기본 형태는 다음과 같다. 아래 해시는 설명용 예시다.
1
2
3
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 12345
version: 포인터 형식의 명세를 식별한다. 설치된 Git LFS 프로그램 버전이 아니다.oid: 실제 파일 내용의 SHA-256 해시다.size: 실제 파일 크기이며 단위는 바이트다.
version에 URL이 보이지만 실제 파일의 다운로드 주소는 아니다. 클라이언트는 저장소에 연결된 LFS 엔드포인트와 통신해 해당 oid의 파일을 찾는다.
따라서 “별도 볼륨에 파일이 있다”는 비유는 Git 객체 저장소와 실제 파일 보관 공간이 분리된다는 점까지만 유효하다. 사용자 전용 디스크 볼륨이 생성되거나 파일 경로가 그 볼륨에 마운트되는 것은 아니다. GitHub에서는 보통 같은 저장소에 연결된 LFS 서비스를 이용하며, 다른 LFS 서버를 사용하도록 구성할 수도 있다.
파일 경로나 이름보다 내용 해시가 식별 기준이다. 파일 내용이 바뀌면 새로운 oid가 만들어지고, 새 커밋의 포인터는 그 내용을 가리킨다. 내용이 같은 파일은 같은 해시를 가지지만, 저장 공간 공유·과금 범위는 호스팅 서비스 정책과 별개로 확인해야 한다.
3. add·commit·push와 clone에서 일어나는 일
LFS는 파일을 편집하는 방식보다 Git에 넣고 꺼내는 경계를 바꾼다.
파일을 기록하고 올릴 때
.gitattributes의 경로 규칙이 어떤 파일에 LFS 처리를 적용할지 정한다.git add과정에서 LFS의 clean 필터가 실제 내용을 로컬 LFS 캐시에 보관하고 Git Index에는 포인터를 전달한다.git commit은 포인터가 포함된 스냅샷을 기록한다. 이때 서버에 파일이 업로드되는 것은 아니다.- 정상적으로 설치된 LFS의 pre-push 훅은 필요한 실제 파일을 LFS 서버에 업로드하고, Git은 커밋·참조를 원격 저장소로 전달한다.
여기서 clean은 파일 내용을 청소한다는 뜻이 아니라 작업 파일에서 Git 저장 형식으로 변환하는 방향을 가리킨다. 반대 방향은 smudge라고 부른다. 구현에서는 장기 실행 filter process를 사용할 수도 있지만, 두 변환 방향의 의미는 같다.
파일을 받아 열 때
Git 이력을 받은 뒤 체크아웃 과정에서 LFS가 포인터를 읽는다. 로컬 캐시에 실제 파일이 있으면 재사용하고, 없으면 서버에서 다운로드해 작업 폴더에 놓는다.
1
2
3
4
5
6
Git 커밋의 포인터
│ 체크아웃 변환
▼
로컬 LFS 캐시에 해당 oid가 있는가?
├─ 있음 → 캐시에서 실제 파일 제공
└─ 없음 → LFS 서버에서 다운로드 → 캐시·작업 폴더에 제공
일반적인 clone은 과거 모든 버전의 LFS 파일을 한꺼번에 받는 방식이 아니다. 기본적으로 체크아웃에 필요한 내용을 받아 쓰고, 과거 버전으로 전환할 때 해당 파일이 추가로 필요할 수 있다.
LFS가 설치·활성화되지 않았거나 다운로드를 생략하도록 설정했다면 포인터 텍스트만 보일 수 있다. 반대로 필터가 설정되어 있는데 다운로드에 실패하면 체크아웃 오류가 발생할 수도 있다. “Git 이력을 받았다”와 “실제 파일을 모두 확보했다”는 같은 말이 아니다.
4. PPTX·PNG라면 무조건 LFS를 써야 할까
확장자만으로 결정할 필요는 없다. 판단 기준은 파일 크기, 변경 빈도, 과거 버전 보존 필요성이다.
| 파일의 성격 | 우선 검토할 방식 | 이유 |
|---|---|---|
| Markdown·소스·텍스트 설정 | 일반 Git | 줄 단위 diff와 병합에 적합 |
| 작고 거의 바뀌지 않는 아이콘·PNG | 일반 Git도 충분 | 별도 다운로드 경로를 운영할 필요가 작음 |
| 반복 수정하는 큰 PPTX·디자인 파일·스크린샷 | Git LFS | 바이너리 변경 이력을 Git 객체와 분리해 관리 |
| 재생성 가능한 빌드 결과·캐시 | 추적 제외 또는 아티팩트 저장소 | 소스 이력에 영구 보관할 이유가 약함 |
| 배포용 대용량 결과물 | Release 자산·패키지/객체 저장소 등 | 소스 버전 관리와 배포 파일 제공의 목적이 다름 |
PNG와 PPTX는 이미 압축된 형식이라 Git의 압축·델타 저장으로 얼마나 줄어들지는 실제 변경 내용에 따라 달라진다. 일반 Git이 바이너리를 저장하지 못하는 것은 아니다. LFS는 파일이 커지거나 자주 바뀔 때 이력과 전송을 어떻게 관리할지 선택하는 방식이다.
또 LFS를 도입해도 PPTX의 의미 있는 슬라이드 diff나 자동 병합 기능이 생기는 것은 아니다. 바이너리의 변경 의도·검토 결과는 Markdown 같은 텍스트 기록과 함께 남기는 편이 좋다.
이미 과거 커밋에 들어간 큰 blob은 LFS 추적 규칙을 추가하는 것만으로 사라지지 않는다. 과거 버전까지 전환하려면 별도 마이그레이션이 필요하고, 이력을 재작성하는 방식은 커밋 ID와 협업 흐름에 영향을 준다.
가져올 이력 깊이를 줄이는 shallow clone과도 목적이 다르다. 이 구분은 Git Shallow Clone에서 이어서 볼 수 있다.
5. 저장소는 가벼워져도 보관 비용이 없어지는 것은 아니다
LFS는 실제 파일을 없애지 않는다. Git의 객체 이력에서 분리할 뿐이다.
GitHub 공식 문서 기준으로 변경된 LFS 파일을 push하면 새 버전 전체 크기가 저장 공간 사용량에 반영된다. 예를 들어 20 MB 파일을 수정해 서로 다른 버전 10개를 업로드했다면, 현재 파일이 20 MB라고 해서 보관해야 할 내용도 20 MB라고 볼 수 없다. 이 예에서는 버전 내용만 약 200 MB가 된다.
다운로드에는 전송량도 발생한다. 공동 작업자의 clone뿐 아니라 CI가 LFS 파일을 받을 때도 고려해야 한다. GitHub에서는 저장소 소유자의 사용량으로 계산되므로 공개 저장소라고 무제한 무료 배포 공간이 되는 것은 아니다.
2026-10-29 확인한 GitHub LFS 과금 문서는 무료 제공량을 넘는 사용량 기반 과금과 예산 설정에 따른 사용 차단을 설명한다. 포함 용량·가격은 바뀔 수 있으므로 여기서는 숫자를 고정하지 않는다. 실제 도입 전에는 다음을 확인한다.
- 저장소 소유 계정의 포함 저장 공간과 다운로드 전송량
- 변경 버전의 누적 크기와 CI·협업자의 다운로드 빈도
- 초과 사용 예산·알림 및 한도 도달 시 동작
6. 백업은 Git 이력과 LFS 내용을 함께 보존해야 한다
커밋에 포인터가 남아 있어도 실제 LFS 파일이 유실되면 원본 내용을 복원할 수 없다. 해시는 내용을 식별하고 검증하는 값이지 파일을 다시 생성하는 재료가 아니다.
따라서 Git 이력만 복제한 mirror나 bundle을 LFS까지 포함한 완전한 백업으로 간주하면 안 된다. 현재 작업 폴더를 복사하는 것도 과거 버전 전체를 보존하지는 않는다.
백업의 완료 기준은 다음 세 가지다.
- 복원하려는 커밋·태그 등 Git 이력이 보존되어 있다.
- 그 범위의 포인터가 참조하는 실제 LFS 내용도 보존되어 있다.
- 원래 서버나 기존 로컬 캐시에 의존하지 않는 별도 환경에서 실제 파일로 복원되는지 확인했다.
GitHub의 소스 ZIP·tarball에 LFS 실제 파일이 포함되는지도 저장소 설정에 따라 달라질 수 있다. 다운로드한 압축 파일이라는 이유만으로 실제 바이너리까지 들어 있다고 가정하지 않는다.
이미 파일이 유실된 경우에는 LFS Object가 사라진 저장소의 Subtree 복구를 참고한다. 다운로드를 생략해 포인터만 가져오는 것과 실제 파일을 복구하는 것은 다른 작업이다.
정리
Git LFS를 이해할 때 기억할 관계는 하나다.
Git은 파일 버전의 식별 관계를 기록하고, LFS 저장 공간은 그 버전의 실제 내용을 보관한다.
이 구조는 큰 바이너리와 소스 이력을 함께 관리하는 데 유용하지만, 실제 파일의 저장 비용·전송·백업 책임까지 없애지는 않는다. 확장자 전체를 기계적으로 LFS로 바꾸기보다, 버전 관리할 가치와 파일 변경 특성을 먼저 판단해야 한다.
관련 글과 공식 자료
- Git과 GitHub가 알아보는 특별한 파일들:
.gitattributes와 다른 설정 파일의 역할 - Git 개념 지도 — devkit: 상태·참조·이력의 전체 좌표
- Git LFS Specification: 포인터 형식과 clean/smudge 동작
- About Git Large File Storage: GitHub의 저장 방식과 소스 압축 파일 설정
- Git Large File Storage billing: 저장 공간·다운로드 전송량 계산