TL;DR

  • Git은 대용량 바이너리 이력 관리에 약하고, GitHub는 100MB 초과 파일 푸시를 거부한다.
  • Git LFS는 포인터만 Git에 남기고 실제 바이너리는 별도 스토리지로 분리해 CI와 협업을 유지한다.
  • 다만 LFS 대역폭·비용·fork PR 리스크까지 봐야 하며, 상황에 따라 외부 아티팩트 저장소가 더 낫다.

1. 개념

Git LFS는 대용량 바이너리 자체를 Git 히스토리에 넣지 않고, 포인터 파일만 Git에 저장한 뒤 실제 파일은 별도 LFS 스토리지에서 내려받도록 만드는 확장이다.

2. 배경

대형 ML 모델, 미디어 자산, 빌드 산출물 같은 바이너리는 Git의 압축·이력 모델과 맞지 않는다. 특히 GitHub의 100MB 푸시 제한과 CI fresh checkout 환경이 겹치면 일반 Git만으로는 배포 파이프라인이 쉽게 깨진다.

3. 이유

대용량 바이너리를 일반 Git에 넣으면 저장소 비대화, 느린 clone, 푸시 거부, CI 실패가 한 번에 발생한다. LFS가 언제 적합하고 언제 S3나 Play Asset Delivery 같은 대안으로 넘어가야 하는지 판단 기준이 필요하다.

4. 특징

  • 포인터 파일, clean/smudge filter, Batch API로 동작하는 저장 구조
  • GitHub 용량 제한과 CI 대역폭 리스크를 함께 다루는 운영 관점
  • S3/R2, Hugging Face, DVC, Play Asset Delivery 등 대안 비교 포함

5. 상세 내용

Git LFS 대용량 바이너리 패키징 완전가이드

작성일: 2026-05-04 카테고리: DevTools / Git / VCS / CI-CD / Storage 트리거: cooking-assistant repo 2026-05-02 사건 — model.int8.onnx(228MB)가 GitHub 100MB 제한에 걸려 첫 커밋 reject 후, CI Android 빌드가 깨지자 18분 만에 Git LFS로 전환. “Not-tested: CI LFS bandwidth quota” 잠재 리스크 명시. 포함 내용: SHA-1 객체 모델, packfile, delta compression, GitHub 50/100MB 경고/거부 한도, 25MiB 브라우저 한도, 1GB/5GB 레포 권장, .gitattributes filter (clean/smudge), LFS pointer file, OID, HTTPS Batch API spec, GitHub LFS 10GiB Free / 250GiB Team 무료 quota, metered billing 2024 전환, Cloudflare R2 + lfs-proxy, Hugging Face Xet, git-annex, DVC, lakeFS, Plastic SCM (Unity VCS), Perforce Helix Core, Microsoft GVFS / Scalar, Meta Mononoke / Sapling, BFG Repo-Cleaner, git lfs migrate import/export, GIT_LFS_SKIP_SMUDGE, actions/checkout lfs:false 패턴, actions/cache LFS 캐시, nschloe/action-cached-lfs-checkout, Play Asset Delivery (install/fast-follow/on-demand), iOS On-Demand Resources, Background Assets, fork PR bandwidth 사보타주, streamlink 사례, Linus Torvalds BitKeeper 사건, cooking-assistant 5월 2일 ONNX 패키징 분석


1. 발생 사례 — cooking-assistant 2026-05-02

1.1 사건 타임라인

┌─────────────────────────────────────────────────────────────────┐
│       cooking-assistant ONNX 모델 패키징 트러블 (2026-05-02)     │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  08:23  e8047cd  feat(voice): enable local stt validation       │
│         └─ sherpa-onnx 도입, 228MB INT8 모델 필요                │
│         └─ "Constraint: ASR model binaries are not committed"   │
│         └─ .gitignore에 model.int8.onnx                          │
│                                                                  │
│           │                                                      │
│           ▼                                                      │
│                                                                  │
│  08:37  f89d913  fix(voice): include sherpa tokens path          │
│         └─ tokens.txt 추가 (이건 일반 텍스트, 1MB)               │
│         └─ "Rejected: Commit model.int8.onnx                     │
│             | exceeds normal GitHub binary limits"               │
│                                                                  │
│           │                                                      │
│           │   18분                                               │
│           ▼                                                      │
│                                                                  │
│  08:55  6ba25bd  feat(voice): package local stt model via lfs   │
│         └─ "CI Android builds need the local SenseVoice         │
│            files in StreamingAssets"                             │
│         └─ ⇒ Git에 안 올리면 CI 빌드 깨짐                        │
│         └─ ⇒ 일반 Git에 못 올림 (>100MB)                         │
│         └─ ⇒ 결론: Git LFS                                       │
│         └─ Tested: git lfs fsck                                  │
│         └─ Tested: staged file is LFS pointer                    │
│         └─ ⚠️  Not-tested: CI LFS bandwidth quota                 │
└─────────────────────────────────────────────────────────────────┘

1.2 두 단계 의사결정

1단계 (08:37):
  - 모델은 빌드 인풋이지 repo 산출물 아님 → .gitignore
  - GitHub 100MB 제한이 reject 사유로 명시됨
  - 결정: 로컬 빌드용으로만, Git에 커밋 안 함

2단계 (08:55):
  - CI는 fresh checkout으로 빌드 → 로컬 파일 의존 불가
  - StreamingAssets에 모델 없으면 APK 빌드 자체가 깨짐
  - 1단계 결정 번복 필요
  - 옵션 검토:
      (a) Git LFS  ← 채택 (가장 익숙, 즉시 적용 가능)
      (b) S3/R2 + 빌드 스크립트
      (c) Hugging Face Hub에서 빌드 시 다운로드
      (d) Play Asset Delivery로 모델을 APK에서 분리
  - "Not-tested: CI LFS bandwidth quota" 잠재 리스크 인정

이 문서는 (a)~(d) 의사결정 트리의 풀 컨텍스트, LFS bandwidth quota가 실제로 어떻게 폭발하는가, 그리고 현 시점에서 cooking-assistant가 (b)~(d)로 이전할 가치가 있는가를 다룬다.


2. 용어 사전

2.1 Git 내부

용어 풀이
SHA-1 object Git의 모든 콘텐츠 단위(blob, tree, commit, tag)를 SHA-1 해시로 식별
blob 파일 콘텐츠 자체 (디렉토리/이름 정보 없음)
packfile 여러 객체를 압축 + 델타 압축한 단일 파일 (.git/objects/pack/*.pack)
delta compression 비슷한 객체 간 차이만 저장하는 압축 기법
shallow clone 최근 N개 커밋만 받기 (--depth N)
sparse checkout 워킹 트리에 일부 디렉토리만 체크아웃
filter .gitattributes로 지정하는 콘텐츠 변환 훅 (clean/smudge)

2.2 Git LFS

용어 풀이
LFS pointer file ~130바이트 텍스트 — Git에는 이것만, 실제 바이너리는 LFS 서버
OID Object ID — LFS 파일의 SHA-256 해시
clean filter git add 시 실제 파일 → 포인터 변환
smudge filter git checkout 시 포인터 → 실제 파일 복원
HTTPS Batch API LFS 전송 프로토콜 (GitHub LFS 오픈 스펙)
git lfs migrate import 일반 Git 객체 → LFS로 이전
git lfs migrate export LFS → 일반 Git으로 되돌리기
GIT_LFS_SKIP_SMUDGE checkout 시 LFS 다운로드 스킵 환경변수

2.3 클라우드 스토리지 / 가격 단위

용어 풀이
GiB 2^30 bytes (≈1.074 GB) — GitHub LFS 공식 단위
egress 외부로 나가는 트래픽 — Cloudflare R2의 핵심 차별점 (egress $0)
Metered billing 종량제 (2024 GitHub LFS 전환)
Data Pack 구형 GitHub LFS 선불 단위 ($5 / 50GiB)

3. Git의 바이너리 약점

3.1 SHA-1 객체 모델의 구조적 문제

┌─────────────────────────────────────────────────────────────────┐
│          왜 Git은 바이너리를 못 다루나                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Git의 핵심 가정:                                                │
│  "모든 파일의 모든 버전을 .git/objects에 SHA-1 blob으로 저장"     │
│                                                                  │
│  텍스트:                                                         │
│    v1: "hello world\n"                                           │
│    v2: "hello there\n"                                           │
│    → packfile에서 delta로 저장: "world → there" (몇 바이트)      │
│                                                                  │
│  바이너리 (250MB ONNX):                                          │
│    v1: [무작위 가중치 250MB]                                     │
│    v2: [재학습 후 가중치 250MB]                                  │
│    → 압축/인코딩된 포맷이라 byte 패턴 없음                       │
│    → delta 시도해도 거의 효율 없음                               │
│    → 두 버전 합쳐 ~500MB packfile 누적                           │
│                                                                  │
│  10번 재학습 = 2.5GB packfile = clone 시 모두 다운로드           │
└─────────────────────────────────────────────────────────────────┘

3.2 결정적 약점들

약점 영향
전체 history clone 250MB × 10 = 2.5GB 다운로드
delta 무효 packfile이 거의 줄지 않음
512MB 이상 delta off Git 기본 동작상 큰 객체는 delta 시도 자체 안 함
diff/merge 불가 협업 시 충돌 해결 불가
packfile 재생성 비용 repacking 시 디스크/CPU 스파이크
GitHub 운영 부하 호스팅 측에서 스토리지/CDN 비용 폭증

3.3 결론

Git은 1970년대 텍스트 소스코드를 위해 2005년 설계됐다. 250MB ONNX 모델은 이 모델의 적이다.


4. GitHub 100MB 제한 역사

4.1 현행 제한 (2026-05 기준)

상황 한도 동작
브라우저 업로드 25 MiB 거부
CLI push 경고 50 MiB 경고만, push 성공
CLI push 거부 100 MiB push 완전 거부 (hard limit)
레포 총량 권장 1 GB 이하
레포 총량 상한 5 GB 권고 초과 시 GitHub 연락 요청

중요: 100MB 초과 파일이 현재 워킹 트리에 없어도, 과거 커밋 히스토리에 존재하면 push가 거부된다. cooking-assistant 첫 시도가 reject된 이유가 정확히 이것.

4.2 왜 100MB인가

GitHub가 공식 문서에서 직접 설명한 적은 없으나, 커뮤니티 분석과 기술적 맥락:

  1. Packfile 운영 비용: 100MB 객체가 버전마다 누적되면 packfile 재생성 + CDN 트래픽 + 스토리지가 기하급수적 폭증
  2. Actions 빌드 비용: 모든 CI runner가 clone 시마다 큰 객체 다운로드 → GitHub 네트워크 비용 증가
  3. LFS 유도선: 2015년 4월 LFS 발표와 동시에 100MB 제한 공식화 → 큰 파일을 유료 LFS로 자연 유도

4.3 등장 시점

2015.02  Andrew Kirkpatrick 블로그에 100MB 에러 메시지 등장
         → 이미 시행 중이었음을 시사
2015.04  GitHub Git LFS 1.0 공식 발표 (Rick Olson)
2015.10  Git LFS v1.0.0 정식 릴리즈, GitHub.com 전체 적용

100MB 제한과 LFS 발표는 거의 동시에 정책화됐다.

4.4 cooking-assistant ONNX의 위치

model.int8.onnx 약 228MB
  > 25 MiB 브라우저 한도        (×2.3 초과)
  > 50 MiB push 경고            (×4.6 초과)
  > 100 MiB push 거부 (hard)    (×2.3 초과)  ← 이 선이 reject 사유

5. Git LFS 동작 원리

5.1 등장 배경

2015.04.08  Rick Olson (GitHub) 공식 블로그 발표
            MIT 라이선스 오픈소스
            Atlassian, Microsoft 등 Git 커뮤니티와 협력
2015.10.01  Git LFS v1.0.0 정식 릴리즈

5.2 동작 메커니즘

┌─────────────────────────────────────────────────────────────────┐
│              Git LFS clean / smudge 필터                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Step 1. .gitattributes 설정                                     │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ *.onnx filter=lfs diff=lfs merge=lfs -text              │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                  │
│  Step 2. git add model.int8.onnx                                 │
│      │                                                           │
│      │  [clean filter 호출]                                      │
│      ▼                                                           │
│   ┌──────────────────────────────┐    ┌──────────────────┐     │
│   │ 실제 228MB 바이너리          │    │ ~130 byte       │     │
│   │ → SHA-256 해시 계산          │ →  │ pointer file:   │     │
│   │ → LFS 서버에 업로드          │    │   version       │     │
│   └──────────────────────────────┘    │   oid sha256:.. │     │
│                  │                     │   size 228...   │     │
│                  │                     └──────────────────┘     │
│                  │                              │                │
│                  ▼                              ▼                │
│           [LFS 스토리지]                 [Git 저장소]           │
│           (S3/Azure Blob 등)            (.git/objects)           │
│                                                                  │
│  Step 3. git push                                                │
│      → 실제 바이너리는 LFS 서버로 (HTTPS Batch API)             │
│      → Git에는 130바이트 pointer만                               │
│                                                                  │
│  Step 4. git checkout (다른 사람)                                │
│      │  [smudge filter 호출]                                     │
│      ▼                                                           │
│      pointer 읽기 → OID로 LFS 서버에서 다운로드 → 워킹 트리에   │
└─────────────────────────────────────────────────────────────────┘

5.3 Pointer File 실제 모습

version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 261534478

이 텍스트 3줄(약 130바이트)이 cooking-assistant의 Git 저장소에 있는 모든 것. 실제 228MB는 GitHub LFS 스토리지에 SHA-256 OID로 참조된다.

5.4 LFS 호스팅 옵션

호스팅 무료 한도 비고
GitHub LFS Free 10GiB / Team 250GiB 가장 간편
GitLab LFS 인스턴스별 자체호스팅 가능
Bitbucket LFS 1GB 기본 2016 도입
AWS CodeCommit LFS 지원 S3 연동
자체 호스팅 lfs-test-server, gitea, Gitea LFS
Hugging Face Hub 사실상 무제한 Git+LFS 호환, ML 모델 표준
Cloudflare R2 + lfs-proxy egress $0 가장 저렴한 자체 운영

6. GitHub LFS Quota 상세

6.1 2024 metered billing 전환 후 현황

플랜 무료 Storage 무료 Bandwidth/월
Free / Pro 10 GiB 10 GiB
Team 250 GiB 250 GiB
Enterprise Cloud 250 GiB 250 GiB

파일당 최대 크기:

플랜 LFS 파일 최대
Free 2 GB
Team 4 GB
Enterprise 5 GB

6.2 종량제 단가 (2024 Enterprise 기준)

항목 단가
Storage $0.07 / GiB-월
Bandwidth $0.0875 / GiB
구형 Data Pack $5 / 50 GiB 묶음 (단종 진행 중)

6.3 핵심 규칙 (가장 자주 잊는 것)

✓ 업로드(push)는 bandwidth 0 소비
✗ 다운로드만 소비 — pull, clone, Actions checkout, Web UI 다운로드 모두
✓ Bandwidth는 repo 소유자 quota에서 차감
   → fork한 외부 contributor가 clone해도 원본 소유자가 부담
✗ Free quota 소진 시 해당 월 LFS 완전 차단 (push/pull 둘 다)

6.4 cooking-assistant 비용 시뮬레이션

모델 228MB, 가정: PR 50개 × 2 matrix(arm64/x86_64) + nightly 30 = 130회/월

Bandwidth 소비:
  228MB × 130회 = ~29.6 GiB / 월

Free 플랜 (10 GiB):
  19.6 GiB 초과
  19.6 × $0.0875 = $1.72 / 월   (현재는 큰 부담 아님)

구형 1GB Data Pack 시절:
  1GB ÷ 228MB ≈ 4회 빌드 만에 소진
  → "Bandwidth quota exceeded"로 LFS 차단
  → 며칠 만에 push/pull 모두 막힘

Team 플랜 (250 GiB):
  여유 충분 — 단 fork PR 트래픽이 폭증하면 위험

6.5 차단 알림

이메일 자동 발송:
  - 80% 도달
  - 90% 도달
  - 100% 도달 (이 시점부터 LFS 차단)

확인 방법:
  GitHub UI → Settings → Billing and plans → Git LFS data

7. Bandwidth 폭발 시나리오

7.1 자주 마주치는 함정

시나리오 소비 (228MB 모델 기준)
PR 빌드 매 커밋 fresh checkout 228MB × 커밋 수
Matrix 빌드 (API level × ABI) 228MB × N
Dependabot PR 빌드 228MB × 매 PR
fork contributor PR 228MB × PR 수, 원본 소유자 quota 차감
Nightly scheduled build 228MB × 30일
actions/checkout lfs: true 기본 매 job마다 재다운로드
GitHub Web UI에서 LFS 파일 다운로드 228MB / 다운로드

7.2 실제 사고 사례

streamlink 프로젝트 (오픈소스):

  • 외부인이 fork + clone 반복으로 LFS bandwidth 소진
  • 원본 repo 소유자의 LFS 서비스가 자동 비활성화
  • 정상 사용자도 LFS 파일을 받지 못하는 상태

Esteban Garcia 사례:

  • 25GB 바이너리 + 월 1,300회 파이프라인 실행
  • GitHub LFS 비용이 월 $3,000까지 치솟음
  • S3 기반 LFS 프록시 캐시 도입 후 월 $7로 다운

7.3 GIT_LFS_SKIP_SMUDGE 트릭

# checkout 시 LFS 다운로드를 완전 스킵
GIT_LFS_SKIP_SMUDGE=1 git clone <repo>

# 나중에 필요한 파일만 명시적으로
git lfs pull --include="app/src/main/assets/models/asr_model.onnx"

이 한 줄로 CI 비용을 자릿수 단위로 줄일 수 있다.


8. CI/CD 베스트 프랙티스

8.1 GitHub Actions — 위험한 기본값

# 위험: 매 job마다 LFS 전체 재다운로드
- uses: actions/checkout@v4
  with:
    lfs: true   # ← bandwidth 빠르게 소진

lfs: true는 기본값은 아니지만, 명시적으로 켜면 매 workflow run마다 전체 LFS 객체를 다운로드. 캐시 없이 사용하면 가장 비싼 패턴.

8.2 권장 패턴 — 캐시 + 선택적 fetch

steps:
  - name: Checkout (LFS 스킵)
    uses: actions/checkout@v4
    with:
      lfs: false      # smudge 비활성화
      fetch-depth: 1  # shallow clone

  - name: LFS 파일 목록으로 캐시 키 생성
    run: |
      git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id

  - name: LFS 캐시 복원
    id: lfs-cache
    uses: actions/cache@v4
    with:
      path: .git/lfs
      key: lfs-$-$
      restore-keys: |
        lfs-$-

  - name: LFS pull (캐시 미스 시에만)
    run: git lfs pull

동작 원리: LFS 파일 내용이 바뀌지 않으면 .lfs-assets-id의 해시가 동일 → 캐시 히트 → bandwidth 0 소비. 모델이 업데이트될 때만 실제 LFS 다운로드.

8.3 검증된 서드파티 액션

- uses: nschloe/action-cached-lfs-checkout@v1
  with:
    include: "*.onnx"

8.4 Selective Fetch — 필요한 모델만

- name: Checkout (LFS 완전 스킵)
  env:
    GIT_LFS_SKIP_SMUDGE: "1"
  uses: actions/checkout@v4

- name: 특정 모델만 fetch
  run: |
    git lfs fetch --include="app/src/main/assets/models/asr_model.onnx"
    git lfs checkout app/src/main/assets/models/asr_model.onnx

여러 모델 중 현재 빌드에 필요한 것만 가져오는 전략. GIT_LFS_SKIP_SMUDGE=1로 자동 다운로드 차단 후, 필요한 파일만 명시 fetch.

8.5 GitLab CI / Jenkins 동등 패턴

플랫폼 환경변수 / 옵션
GitLab CI GIT_LFS_SKIP_SMUDGE: "1"
Jenkins (Git LFS Plugin) “Git LFS pull after checkout” 체크 해제
CircleCI git lfs install --skip-smudge 후 명시 pull
Travis CI before_install: git lfs install --skip-smudge

8.6 Fork PR 처리

# fork PR에서는 LFS 다운로드를 막아 원본 quota 보호
on: pull_request

jobs:
  build:
    if: github.event.pull_request.head.repo.full_name == github.repository
    # fork가 아닌 경우만 LFS 사용

또는 fork PR은 LFS 없는 빌드만 수행하고, 메인테이너 승인 후 별도 workflow로 풀 빌드.


9. 외부 스토리지로 우회

9.1 Cloudflare R2 — 가장 저렴

항목 GitHub LFS Cloudflare R2
무료 Storage 10 GiB 10 GiB/월
무료 Bandwidth 10 GiB 무제한 (egress $0)
초과 Storage $0.07 / GiB $0.015 / GiB
초과 Bandwidth $0.0875 / GiB $0

실제 마이그레이션 사례 (David Bushell, 2024.07):

# 1. 기존 LFS 객체 전부 가져오기
git lfs fetch --all

# 2. .git/config 엔드포인트를 R2 proxy로 변경
git remote set-url origin https://r2-lfs-proxy.example.com/repo.git

# 3. R2로 push
git lfs push --all origin

이 사례에서 1주일 만에 1GB 무료 한도 소진 → R2로 이전 후 사실상 0원.

9.2 Hugging Face Hub — ML 모델 최적

- name: Download model from Hugging Face
  run: |
    pip install huggingface_hub
    python -c "
    from huggingface_hub import hf_hub_download
    hf_hub_download(
      repo_id='your-org/cooking-assistant-models',
      filename='asr_model.onnx',
      local_dir='app/src/main/assets/models'
    )"

Hugging Face의 매력:

  • 공개 모델 무료 + bandwidth 사실상 무제한
  • 2024.08 XetHub 인수 후 Xet 스토리지 도입 — 바이트 단위 중복 제거, Git LFS 대비 10x 성능
  • 2025.05+ 신규 저장소는 기본값으로 Xet 사용
  • CloudFront CDN 글로벌 배포

cooking-assistant ONNX 모델이 sherpa-onnx 공식 모델이라면 이미 Hugging Face Hub에 있다 — repo에 안 올려도 되고 빌드 시 다운로드만 하면 된다.

9.3 비용 시뮬레이션 (228MB 모델, 월 130회 빌드)

스토리지 방식 월 비용 비고
GitHub LFS Free 플랜 $0 + $1.72 초과분 10GB 무료 + 종량제
GitHub LFS 구형 1GB ~$120 (Data Pack 24개) 1GB 4회 만에 소진
Cloudflare R2 + LFS Proxy ~$0.05 (Storage만) Egress $0
Hugging Face Hub (공개) $0 무제한
S3 us-east-1 $2.66 (egress) $0.09/GB × 29.6GB

10. 대안 기술 비교

10.1 종합 표

도구 출시 핵심 컨셉 적합 상황 단점
Git LFS 2015 smudge/clean 필터 + 외부 스토리지 일반 바이너리, 간편함 bandwidth 비싼 quota
git-annex 2010 분산, symlink, 다양한 백엔드(S3/SSH/Bittorrent) 연구 데이터, 개인 아카이브 학습 곡선 가파름
DVC 2017 ML 특화, 파이프라인 추적, 실험 재현 MLOps, ML 모델+데이터셋 추가 인프라 필요, 2GB+ push 느림
Hugging Face Hub 2019 Git+LFS 위 자체 인프라, Xet 스토리지 공개 ML 모델 표준 사내 비공개는 유료
lakeFS 2020 Git-like 데이터 레이크, S3 호환 대규모 ML 데이터셋 모델 단독엔 과잉
Perforce Helix Core 1995 바이너리 델타 + 파일 잠금 게임/영상 대규모 팀 유료, Git 워크플로우와 단절
Plastic SCM (Unity VCS) 2006 게임 에셋 특화, Unity Editor 통합 Unity 프로젝트 Unity 생태계 종속
Microsoft GVFS / Scalar 2017 가상 파일시스템, 부분 체크아웃 거대 모노레포 (300GB+) Windows 우선 (Scalar는 macOS 추가)
Meta Sapling 2022 Mercurial 기반 internal SCM, 오픈소스화 Meta 사내 거대 repo 학습 곡선, 채택 적음
S3/R2/B2 + 스크립트 단순, 저렴 CI 잦고 모델 자주 안 바뀜 Git과 분리, 버전 수동

10.2 git-annex vs DVC 핵심 차이

git-annex:
  - "파일이 어디에 있는지 추적"
  - 매우 유연 (다중 백엔드, P2P, BitTorrent)
  - symlink 기반 — Windows에서 까다로움
  - 단일 파일 중심

DVC:
  - "ML 워크플로우 전체 추적"
  - 파이프라인 (dvc.yaml) + 실험 + 메트릭
  - S3/GCS/Azure 백엔드
  - MLOps 컨텍스트

10.3 왜 게임/영상 업계는 Git을 안 쓰나

Perforce Helix Core가 게임 개발 사실상 표준인 이유:

기능 Git LFS Perforce
바이너리 델타 ✗ (LFS는 전체 파일 새 버전) ✓ (실제 변경 부분만)
파일 잠금 (exclusive lock) ✓ (Maya 파일 잠그면 다른 사람 수정 불가)
선택적 체크아웃 sparse-checkout 가능 ✓ 기본 동작
동시 커밋 처리 제한적 10,000+ 동시
10TB+ repo 지원 비효율
라이선스 오픈소스 유료

대규모 게임 회사 채택 사례:

  • Riot Games (League of Legends): Perforce
  • EA, Ubisoft: Perforce + 자체 빌드 시스템
  • CD Projekt RED (Cyberpunk 2077): Perforce + 250TB+ 에셋
  • Unity 인디 게임: Plastic SCM (Unity가 인수)

cooking-assistant는 Unity 베이스이므로 장기적으로는 Plastic SCM이 합리적 선택지일 수 있으나, 현 단계에서는 과잉.


11. 모바일 패키징과의 통합 — Git LFS를 완전 우회하는 길

11.1 핵심 통찰

CI bandwidth를 절감하는 것은 국부 최적화다. 모델을 APK 자체에서 분리하면 근본 해결이 된다.

228MB ONNX가 APK에 포함되지 않으면 CI가 LFS를 fetch할 이유 자체가 없다.

11.2 Android Play Asset Delivery (PAD)

┌─────────────────────────────────────────────────────────────────┐
│             Play Asset Delivery 구조                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  AAB (App Bundle)                                                │
│   ├── base APK (앱 코어, 작음)                                   │
│   └── asset pack: models-pack                                    │
│         ├── delivery: install-time | fast-follow | on-demand     │
│         └── asr_model.onnx (228MB)                               │
│              → Google Play CDN이 배포                             │
│              → LFS bandwidth = 0                                  │
└─────────────────────────────────────────────────────────────────┘
모드 동작
install-time 앱 설치 시 함께 다운로드, APK 크기 증가 (실질 무한)
fast-follow 설치 직후 자동 백그라운드 다운로드
on-demand 앱이 필요할 때 명시적 요청

CI 관점:

  • 빌드 시 모델이 AAB의 별도 모듈
  • CI에서 base APK 모듈만 빌드 시 LFS checkout 불필요
  • asset pack 모듈은 별도 Gradle 모듈로 분리

11.3 첫 실행 시 CDN 다운로드 패턴

suspend fun ensureModelAvailable(context: Context) {
    val modelFile = File(context.filesDir, "asr_model.onnx")
    if (!modelFile.exists() || !verifyChecksum(modelFile)) {
        downloadFromCDN(
            url = "https://cdn.example.com/models/v1.2.3/asr_model.onnx",
            destination = modelFile,
            expectedSha256 = "abc123..."
        )
    }
}

비교:

방식 LFS bandwidth APK 크기 오프라인 가능 첫 실행 UX
LFS in repo (현재) 높음 즉시
PAD install-time 0 큼 (Play 분리) 즉시 (설치 길어짐)
PAD on-demand 0 작음 ⚠️ 첫 실행 후 로딩 UI 필요
자체 CDN 다운로드 0 작음 ⚠️ 첫 실행 후 로딩 UI 필요

11.4 cooking-assistant 권고

현재:
  StreamingAssets/SherpaOnnx/sense-voice/model.int8.onnx
    + Git LFS
    + CI에서 매 빌드마다 다운로드 (Not-tested 리스크)

단기 개선 (현 구조 유지 + 비용 통제):
  ✓ actions/cache로 .git/lfs 캐싱
  ✓ lfs: false + 명시 pull
  ✓ fork PR LFS skip
  → 월 비용 $0.05 ~ $1.72 수준 유지

중기 개선 (구조 변경):
  ○ Hugging Face Hub로 모델 이전
    → 빌드 시 hf_hub_download
    → repo에서 model.int8.onnx 완전 제거
    → git lfs migrate export로 history 정리
  ○ 또는 Cloudflare R2 + 빌드 스크립트

장기 개선 (모바일 UX 변경):
  ○ Play Asset Delivery fast-follow
    → APK 작아짐, OTA 모델 업데이트 가능
    → 단 Google Play 외 배포 (사내 빌드, XR 디바이스)에는 부적합

  tokens.txt(1MB)는 어떤 옵션에서도 일반 Git/APK assets에 유지

12. 진화 타임라인

┌─────────────────────────────────────────────────────────────────┐
│             VCS와 대용량 바이너리 관리 연표                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1995    Perforce Helix Core (게임 산업 표준 굳히기 시작)        │
│  2005    Git 공개 — Linus Torvalds, BitKeeper 사건 계기 2주 만   │
│  2006    Plastic SCM (게임 에셋용)                               │
│  2008    GitHub 서비스 시작                                      │
│  2010    git-annex — Joey Hess, 오프그리드 캐빈에서              │
│  2012    git-annex Kickstarter 캠페인, GUI assistant 개발        │
│  2015.02 GitHub 100MB 제한 에러 확인 (외부 블로그)               │
│  2015.04 GitHub Git LFS 1.0 발표 (Rick Olson)                    │
│  2015.10 Git LFS v1.0.0 정식 릴리즈                              │
│  2016    GitLab LFS, Bitbucket LFS 도입                          │
│  2017.05 DVC 0.6 공개 (Iterative.ai)                             │
│  2017.11 Microsoft GVFS — Windows 300GB+ 모노레포 대응           │
│  2018    Microsoft GVFS → VFS for Git (이름 변경)                │
│  2019    Hugging Face Hub 오픈 — Git+LFS 기반 모델 호스팅 표준화 │
│  2020.05 DVC 1.0 정식                                            │
│  2020    VFS for Git → Scalar 전환 (macOS 지원, 가상화 탈피)     │
│  2022    Meta Sapling 오픈소스화 (Mercurial 기반)                │
│  2023    Unity, Plastic SCM을 Unity VCS로 리브랜딩               │
│  2024.06 GitHub Enterprise LFS 종량제 전환                       │
│  2024.08 Hugging Face가 XetHub 인수 → Xet 스토리지              │
│  2024.12 GitHub Free/Pro LFS 종량제 전환 (10GB 무료 상향)        │
│  2025.05 Hugging Face Hub 신규 저장소 기본값 Xet                 │
│  2026.05 cooking-assistant Git LFS로 ONNX 패키징 (현재)          │
└─────────────────────────────────────────────────────────────────┘

13. 빅테크/오픈소스 실전 사례

13.1 Hugging Face

  • Git+LFS 위에 자체 인프라 — 25M+ 모델 호스팅
  • S3 백엔드 + CloudFront CDN
  • 2024.08 XetHub 인수로 Xet 스토리지 도입
    • 바이트 단위 중복 제거
    • LFS 대비 10x 성능
    • 모델 파일이 일부만 바뀌어도 변경된 청크만 전송
  • 공개 모델 bandwidth 무료
  • ML 모델 호스팅의 사실상 표준

13.2 Microsoft — GVFS / VFS for Git / Scalar

2017  GVFS (Git Virtual File System) — Windows 가상 파일시스템 의존
2018  VFS for Git 리브랜딩 (GNOME VFS와 혼동 해소)
2020  Scalar — macOS 지원, 가상화 의존 제거, 일반 Git 위에서 동작

Microsoft는 Windows 소스코드 (300GB+, 4M 파일, 12,000명 엔지니어) 를 Git에서 운영. Scalar는 그 노하우의 일반화.

13.3 Meta — Mononoke / Sapling

  • Mercurial 기반 내부 SCM (Git이 거대 모노레포에 부적합하다고 판단)
  • 2022 Sapling으로 오픈소스화
  • Meta 사내 코드베이스 규모 → 수십 TB

13.4 Google — Piper / Blaze (현재 Bazel)

  • 단일 monorepo 80TB+
  • Piper (자체 VCS) + Blaze (분산 빌드)
  • Bazel은 Blaze의 오픈소스 버전
  • Git을 안 쓰는 대표 사례

13.5 게임 산업

회사 VCS 규모
Riot Games (League of Legends) Perforce TB급 에셋
EA Perforce + 자체 시스템 다수
Ubisoft Perforce 다수
CD Projekt RED (Cyberpunk 2077) Perforce 250TB+
Valve Perforce 다수
인디 (Unity) Plastic SCM (Unity VCS) 다수

게임/AR/VR 대규모 팀이 Git을 외면한 이유: 바이너리 델타 + 파일 잠금 + 부분 체크아웃의 부재.

13.6 LFS Bandwidth 사고

  • streamlink: 외부 fork로 인한 quota 소진 → LFS 자동 비활성화
  • Esteban Garcia 사례: 월 $3,000 → S3 프록시 도입 후 월 $7
  • 여러 오픈소스 프로젝트: GitHub LFS에서 자체 호스팅 / R2 / HF로 마이그레이션

14. 의사결정 + 체크리스트

14.1 ONNX 모델 250MB 의사결정 트리

┌─────────────────────────────────────────────────────────────────┐
│       "이 큰 파일을 어디 둘까" 의사결정 트리                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  파일이 공개 가능한 ML 모델인가?                                  │
│      ├─ YES → Hugging Face Hub                                   │
│      │       (무료, bandwidth 관대, Xet 스토리지)                │
│      │       → repo에는 모델 URL/해시만, 빌드 시 hf_hub_download │
│      │                                                           │
│      └─ NO (사내 비공개)                                         │
│            ├─ ML 모델 자주 바뀜 + MLOps 팀 있음                  │
│            │   → DVC + 사내 S3 (실험 추적까지)                   │
│            │                                                     │
│            ├─ ML 모델 거의 안 바뀜 + CI 자주 돌림                │
│            │   → Cloudflare R2 + 빌드 스크립트                   │
│            │     (egress $0, 가장 저렴)                          │
│            │                                                     │
│            ├─ 팀 작음 + 간편함 우선 + 작은 quota 충분            │
│            │   → Git LFS (단, CI 캐싱 필수)                      │
│            │                                                     │
│            └─ 게임/AR/VR 거대 에셋 (수 TB)                       │
│                → Perforce / Plastic SCM                          │
│                                                                  │
│  모바일/임베디드 배포 컨텍스트도 함께 고려:                       │
│      ├─ Android Play Store 배포 → Play Asset Delivery            │
│      ├─ iOS App Store 배포 → Background Assets (iOS 16+)         │
│      └─ 자체 배포 (XR 디바이스 등) → 첫 실행 CDN 다운로드        │
└─────────────────────────────────────────────────────────────────┘

14.2 즉시 적용 체크리스트 (Git LFS 사용 중)

[ ] actions/checkout에서 lfs: true → lfs: false 변경
[ ] LFS 캐시 step 추가 (.git/lfs 경로, .lfs-assets-id 해시 키)
[ ] git lfs pull 전에 캐시 restore step 배치
[ ] fork PR workflow에서 LFS fetch 조건부 처리
[ ] matrix 빌드에서 LFS 캐시 공유 전략 수립
[ ] LFS bandwidth 90% 알림 이메일 수신 확인
[ ] GitHub Settings → Billing에서 LFS 사용량 모니터링 자동화
[ ] checksum 검증 자동화 (CI 다운로드 후 즉시 검증)
[ ] .gitattributes의 LFS 추적 패턴 검토

14.3 구조적 개선 체크리스트

[ ] 250MB+ 파일이 Git LFS에 진짜 필요한지 재평가
[ ] 공개 모델이면 Hugging Face Hub 이전 검토
[ ] 사내 모델이면 R2/S3 + 스크립트 검토
[ ] 모바일 빌드면 Play Asset Delivery 분리 검토
[ ] 모델 버저닝 전략 (semver 태그 또는 SHA256 해시)
[ ] git lfs migrate export로 history 정리 가능성 확인

14.4 비상 대응 (LFS bandwidth 소진 시)

1. 즉시 대응:
   - GIT_LFS_SKIP_SMUDGE=1 환경변수로 다운로드 차단
   - CI workflow를 curl/wget 다운로드 방식으로 임시 전환

2. 단기 (수 시간 내):
   - 모델 파일을 R2/Hugging Face에 임시 업로드
   - 빌드 스크립트가 외부에서 다운로드하도록 수정
   - 다음 월 billing cycle 재설정 또는 Data Pack 구매

3. 중기 (수 일 내):
   - git lfs migrate export로 LFS에서 일반 Git으로 (또는 그냥 제거)
   - .gitattributes 정리
   - 팀 전체에 force push 영향 공지
   - 외부 contributor에게 re-clone 요청

4. 장기:
   - 외부 스토리지로 영구 이전
   - LFS 사용 정책 문서화

15. cooking-assistant 적용 결론

15.1 현 시점 평가

✅ Git LFS 도입 결정 자체는 합리적
   - 즉시 적용 가능
   - 팀이 익숙
   - 228MB는 LFS의 적정 사용 범위 (5GB 한도 대비 충분)

⚠️ Not-tested: CI LFS bandwidth quota
   - 실제 bandwidth 폭발은 아직 안 일어남
   - 빌드 빈도 × fork PR 수 × matrix 크기에 따라 위험도 변동
   - 한 번 폭발하면 LFS 차단 → 빌드 전면 중단

✅ tokens.txt를 일반 Git에 유지한 결정은 정확
   - 1MB 텍스트 → delta 압축 잘 됨
   - LFS bandwidth 절약

15.2 우선순위 권고

P0 (즉시):
  - actions/cache로 .git/lfs 캐싱 추가
  - GitHub Settings에서 LFS 사용량 모니터링 자동화
  - 80%/90% 알림 활성화 확인

P1 (다음 스프린트):
  - 빌드 횟수 / matrix 크기 측정
  - 월간 LFS bandwidth 추세 그래프
  - Free 플랜이면 Team 업그레이드 vs 외부 이전 비용 비교

P2 (분기):
  - sherpa-onnx 모델이 공개 모델이면 Hugging Face Hub 사용 검토
    (sherpa-onnx 공식 SenseVoice INT8은 이미 HF Hub에 있음)
  - 또는 Cloudflare R2 + 빌드 스크립트 PoC

P3 (장기):
  - Play Asset Delivery 도입 검토 (Google Play 배포 시)
  - 또는 자체 배포면 첫 실행 CDN 다운로드 패턴
  - APK 사이즈 축소가 부수 효과

References

Git / Git LFS 공식

CI 베스트 프랙티스

외부 스토리지 마이그레이션

Hugging Face / Xet

대안 비교

Microsoft / Meta

LFS 마이그레이션

모바일 패키징

사고 사례

모니터링