Git LFS
고친 사람 github-actions[bot]
Git LFS 는 큰 파일을 Git 저장소 밖에 따로 맡겨 두는 확장 프로그램입니다. 저장소에는 그 파일을 가리키는 짧은 파일만 커밋합니다. 그래서 영상이나 디자인 원본을 자주 고쳐도 저장소가 무거워지지 않습니다.
쉽고 빠른 이해
Git LFS 는 큰 파일을 따로 맡기고 저장소에는 가리키는 작은 파일만 남기게 해 주는 확장 프로그램입니다. 외투를 보관소에 맡기고 번호표만 들고 다니는 것과 같습니다. 번호표는 저장소에 들어가는 포인터 파일입니다. 보관소는 큰 파일을 따로 받아 두는 LFS 서버입니다. LFS 는 Large File Storage(대용량 파일 저장소)의 줄임말입니다.
이게 없으면 큰 파일을 고칠 때마다 한 벌씩 저장소에 쌓입니다. 클론하는 사람은 쓰지도 않을 옛 판까지 전부 내려받습니다.
돌아가는 방식은 이렇습니다.
- 커밋할 때 큰 파일 대신 포인터 파일을 넣습니다. 본체는 따로 챙겨 둡니다
- 푸시할 때 본체를 LFS 서버로 올립니다
- 체크아웃할 때 포인터 파일을 보고 본체를 받아 와 원래 파일로 되돌려 놓습니다
대가가 있습니다. 쓰는 사람마다 Git LFS 를 설치해야 합니다. 설치를 빠뜨리면 파일 대신 포인터 파일만 보입니다. Git 서버와 별도로 LFS 서버도 있어야 합니다.
영상이나 디자인 원본처럼 크고 자주 고치는 파일에 씁니다. 소스 코드나 작고 거의 안 바뀌는 파일은 Git 에 그대로 둡니다.
상세
큰 파일이 저장소를 불리는 과정
Git 은 커밋할 때마다 바뀐 파일의 내용을 한 벌씩 새로 저장합니다. 이 한 벌이 블롭입니다. 소스 코드 같은 텍스트 파일은 크기가 작습니다. 블롭이 여러 벌 쌓여도 부담이 적습니다.
이진 파일은 사정이 다릅니다. 이진 파일은 사람이 읽는 글자가 아니라 바이트로 적힌 파일입니다. 영상 · 음원 · 디자인 원본이 그렇습니다. 이런 파일은 조금만 고쳐도 큰 블롭이 하나 더 쌓입니다.
문제는 클론에서 드러납니다. 클론은 서버에 있는 저장소의 이력 전체를 내 컴퓨터로 복사하는 명령입니다. 오늘 필요한 것은 최신판 하나뿐입니다. 그래도 클론은 옛 판을 전부 함께 내려받습니다.
Git LFS 는 본체와 이력을 떼어 놓아 이 문제를 풉니다. 이력에는 작은 파일만 남깁니다. 큰 본체는 따로 둡니다. 필요한 판의 것만 그때그때 받아 옵니다.
본체를 두는 곳이 LFS 서버입니다. Git 저장소를 맡는 서버와는 별도의 저장소입니다. GitHub · GitLab 같은 호스팅 서비스가 Git 서버와 함께 LFS 서버를 제공합니다. 회사 안에 직접 세워 쓰기도 합니다.
Git LFS 는 GitHub 이 만들어 공개한 오픈 소스 프로그램입니다. Git 과는 별개 프로그램이라 따로 설치해야 쓸 수 있습니다.
Git 을 설치할 때 함께 깔리기도 합니다. 설치하면 git lfs 로 시작하는 명령이 생깁니다.
포인터 파일
Git LFS 가 저장소에 남기는 작은 파일이 포인터 파일입니다. 본체가 무엇인지 가리키기만 해서 붙은 이름입니다. 몇 줄짜리 텍스트라서 본체가 아무리 커도 작게 유지됩니다.
아래는 logo.psd 라는 디자인 원본 대신 저장소에 들어가는 포인터 파일입니다.
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 84977953
첫 줄은 포인터 파일 형식의 판을 적습니다. 셋째 줄은 본체의 크기를 바이트로 적습니다.
본체를 찾는 열쇠는 둘째 줄의 oid(object id, 객체 식별자)입니다.
oid 는 본체 내용을 해시 함수에 넣어 얻은 값입니다. 해시 함수는 무엇을 넣든 정해진 길이의 값을 돌려주는 함수입니다.
내용이 한 바이트만 달라도 값이 달라집니다. 그래서 이 값 하나로 본체를 틀림없이 찾을 수 있습니다.
Git LFS 가 쓰는 해시 함수는 SHA-256(Secure Hash Algorithm 256)입니다. 포인터 파일의 oid 앞에 붙은 sha256: 이 그 표시입니다.
이 값은 본체의 내용만으로 정해집니다. 내용이 같은 본체는 값도 같습니다. 같은 파일을 여러 커밋에 넣어도 본체는 한 벌만 저장됩니다. 이렇게 내용으로 이름을 정하는 방식이 내용 주소 지정입니다.
커밋 이력에는 이 포인터 파일만 들어갑니다. 디자인 원본을 백 번 고쳐도 이력에 쌓이는 것은 작은 텍스트 백 개입니다.
파일을 바꿔치는 두 필터
개발자는 평소처럼 git add 와 git checkout 만 합니다. 그 사이 파일은 포인터로 바뀌었다가 다시 본체로 돌아옵니다.
그 일을 맡는 것은 Git 의 필터 기능입니다.
필터는 파일이 저장소를 드나들 때 내용을 한 번 바꿔 주는 프로그램입니다. Git 은 들어갈 때와 나올 때 각각 필터를 걸 수 있게 해 둡니다. Git LFS 는 이 두 필터를 자기 프로그램으로 채웁니다.
인덱스는 다음 커밋에 담을 내용을 모아 두는 준비 구역입니다. git add 가 파일을 여기 올립니다.
파일이 인덱스로 들어갈 때 도는 필터가 클린(clean) 필터입니다. 클린 필터는 큰 파일의 본체를 .git/lfs/objects 폴더에 옮겨 담습니다.
인덱스에는 포인터 파일을 올립니다.
체크아웃을 하면 저장소의 한 판이 작업 폴더에 꺼내집니다.
파일이 작업 폴더로 나올 때 도는 필터가 스머지(smudge) 필터입니다.
스머지 필터는 포인터 파일의 oid 로 본체를 찾아 원래 파일로 되돌려 놓습니다. 내 컴퓨터에 본체가 없으면 LFS 서버에서 받아 옵니다.
어느 파일에 필터를 걸지는 .gitattributes 파일이 정합니다. 저장소 안에 두는 설정 파일입니다. 이 파일도 커밋하므로 팀 전체가 같은 규칙을 씁니다.
준비는 명령 두 개입니다. 첫 명령은 필터를 Git 에 등록합니다. 둘째 명령이 규칙을 적습니다.
git lfs install # 필터를 Git 에 등록
git lfs track "*.psd" # 규칙 한 줄 추가
첫 명령은 컴퓨터마다 한 번 칩니다. 둘째 명령을 치면 .gitattributes 에 아래 줄이 생깁니다.
*.psd filter=lfs diff=lfs merge=lfs -text
filter=lfs 가 두 필터를 거는 부분입니다. 나머지 셋은 이 파일을 텍스트로 다루지 말라는 표시입니다.
비교 · 병합 · 줄바꿈 변환을 텍스트 방식으로 하지 않게 막습니다.
본체가 오가는 두 갈래 길
푸시하고 받아 올 때 포인터 파일과 본체는 서로 다른 서버로 갑니다. 커밋과 포인터 파일은 Git 서버로 갑니다. 본체는 LFS 서버로 갑니다.
푸시는 내 커밋을 서버의 저장소로 올리는 명령입니다. 푸시할 때는 본체가 먼저 올라갑니다.
이 순서는 훅이 만듭니다. 훅은 Git 이 어떤 동작 직전이나 직후에 불러 주는 스크립트입니다. Git LFS 는 푸시 직전에 도는 훅을 걸어 둡니다. 그 훅이 본체를 LFS 서버로 올립니다.
포인터 파일만 먼저 도착하면 동료가 받아 갔을 때 가리키는 본체가 서버에 없습니다. 그래서 본체를 먼저 올립니다.
받는 쪽은 반대 순서입니다. 동료는 Git 서버에서 커밋과 포인터 파일을 받습니다. 체크아웃할 때 스머지 필터가 그 판에 필요한 본체만 LFS 서버에 요청합니다.
sequenceDiagram
participant 내 컴퓨터
participant LFS 서버
participant Git 서버
participant 동료 컴퓨터
Note over 내 컴퓨터: git add 때 본체를 포인터 파일로 바꾼다
내 컴퓨터->>LFS 서버: 푸시 직전 훅이 본체를 올린다
내 컴퓨터->>Git 서버: 커밋과 포인터 파일을 올린다
동료 컴퓨터->>Git 서버: 클론하거나 받아 온다
Git 서버-->>동료 컴퓨터: 커밋과 포인터 파일
동료 컴퓨터->>LFS 서버: 체크아웃한 판의 본체를 달라고 한다
LFS 서버-->>동료 컴퓨터: 본체
Note over 동료 컴퓨터: 포인터 파일을 본체로 되돌려 놓는다
그림에서 보듯 동료가 내려받는 본체는 체크아웃한 판의 것뿐입니다. 옛 판의 본체는 그 판을 꺼낼 때 받습니다. 이력이 길어도 클론이 가벼운 까닭입니다.
이진 파일의 잠금
이진 파일은 둘이 동시에 고치면 합칠 수 없습니다. 텍스트처럼 줄 단위로 견줄 수 없기 때문입니다. 결국 한쪽 작업을 버려야 합니다. Git LFS 에 넣어도 이 점은 그대로입니다.
그래서 Git LFS 는 파일 잠금을 제공합니다. 잠금은 "이 파일은 지금 내가 고치는 중"이라고 서버에 적어 두는 일입니다. 다른 사람이 그 파일을 고친 커밋은 잠금이 풀릴 때까지 푸시가 거절됩니다.
git lfs lock logo.psd # 서버에 잠금 기록
git lfs unlock logo.psd # 잠금 해제
Git 에는 원래 잠금이 없습니다. 각자 고친 뒤 합치는 것이 전제이기 때문입니다. Git LFS 의 잠금은 이 빈틈을 서버 쪽에서 메웁니다. 잠금을 쓰려면 LFS 서버가 이 기능을 지원해야 합니다.
모든 컴퓨터에 설치가 필요한 점
Git LFS 가 없는 컴퓨터에서 클론하면 스머지 필터가 돌지 않습니다. 작업 폴더에 본체 대신 포인터 파일이 놓입니다.
logo.psd 를 읽는 빌드는 그림 대신 세 줄짜리 포인터 파일을 받습니다. 실패는 Git LFS 와 상관없어 보이는 곳에서 납니다.
CI(Continuous Integration, 지속적 통합) 서버도 예외가 아닙니다. CI 서버는 커밋이 올라올 때마다 빌드와 테스트를 자동으로 돌리는 서버입니다. 이 서버에도 Git LFS 를 깔고 본체를 받아 오도록 설정해야 합니다.
서버 용량과 전송량
Git LFS 는 클론을 가볍게 할 뿐 옛 판을 없애지는 않습니다. 옛 판의 본체는 LFS 서버에 모두 남습니다. 호스팅 서비스는 이 저장 용량과 내려받는 양을 Git 저장소와 따로 셉니다. 따로 정한 한도를 넘으면 서비스에 따라 올리기나 받기가 막히거나 요금이 붙습니다.
오프라인 작업의 제약
인터넷이 끊기면 Git 과 Git LFS 의 차이가 드러납니다. Git 은 이력 전체가 내 컴퓨터에 있어 오프라인에서도 옛 판을 꺼낼 수 있습니다. Git LFS 로 관리하는 파일은 받아 두지 않은 판을 꺼낼 수 없습니다. 본체가 서버에만 있기 때문입니다.
이미 커밋한 파일의 이전
git lfs track 은 앞으로 들어올 파일에만 걸립니다. 이미 이력에 들어간 큰 블롭은 그대로 남습니다.
옛 이력까지 옮기려면 git lfs migrate 로 이력을 고쳐 써야 합니다.
이력을 고쳐 쓰면 커밋 해시가 바뀝니다. 커밋 해시는 커밋 내용으로 계산한 커밋의 이름입니다. 고친 지점부터 뒤의 커밋이 모두 새 이름을 받습니다. 그래서 팀원 모두가 새 이력으로 갈아타야 합니다.
쓰는 경우와 안 쓰는 경우
Git LFS 는 크고 자주 바뀌는 이진 파일에 맞습니다. 파일마다 판단이 갈리므로 아래 표로 가립니다.
| 파일 | 선택 | 까닭 |
|---|---|---|
| 자주 고치는 디자인 원본 · 영상 · 음원 | Git LFS | 고칠 때마다 쌓이는 큰 블롭을 이력에서 뺀다 |
| 소스 코드 · 설정 파일 | Git 그대로 | LFS 에 넣으면 줄 단위 비교와 병합을 잃는다 |
| 작고 거의 안 바뀌는 이진 파일 | Git 그대로 | 쌓이는 양이 적어 설치와 서버를 늘릴 만큼 이득이 없다 |
| 큰 파일이 대부분이고 잠금이 일상인 작업 | Perforce 같은 도구도 후보 | 처음부터 큰 파일과 잠금을 전제로 만든 버전관리 도구다 |
표의 마지막 줄은 게임 스튜디오처럼 큰 이진 파일이 저장소의 대부분인 경우입니다. 이때는 Git LFS 로 Git 을 보강하는 방법과 다른 도구를 쓰는 방법을 함께 견줍니다.
관련 항목
Git LFS 가 확장하는 버전관리 도구
Git · 버전관리 · 분산 버전관리 · 저장소 · 원격 저장소
Git LFS 가 기대는 Git 의 장치
.gitattributes · Git 훅 · 인덱스 · 블롭 · 커밋 · 체크아웃
Git LFS 가 본체를 옮기는 명령
포인터 파일이 본체를 찾는 해시
Git LFS 서버를 제공하는 호스팅 서비스
GitHub · GitLab · Bitbucket · Gitea
Git LFS 를 쓸 때 부딪히는 문제
이진 파일 · 병합 충돌 · 잠금 · 이력 재작성 · 지속적 통합
Git LFS 를 대신할 수 있는 다른 수단
git-annex · DVC · Perforce · 서브모듈 · 얕은 클론 · 부분 클론 · 희소 체크아웃
다른 이름: Git Large File Storage · git-lfs · 깃 LFS