Git submodule 서브모듈 실무 | 추가·업데이트·CI·모노레포 대안
이 글의 핵심
서브모듈은 상위 저장소가 특정 커밋 SHA를 가리키는 공유 라이브러리 패턴이며, 클론·CI·버전 핀을 한 번에 설계하지 않으면 팀 전체가 같은 실수를 반복합니다.
들어가며
Git submodule은 한 저장소 안에 다른 Git 저장소를 디렉터리로 포함시키는 방식입니다. 상위 저장소(슈퍼프로젝트)는 하위 저장소의 특정 커밋 SHA만 기록하므로, “의존 라이브러리를 소스로 포함하되 버전을 엄격히 핀한다”는 요구에 맞습니다.
반면 클론 한 번에 모든 것이 끝나지 않고, CI·Docker 빌드에서 초기화 단계를 빼먹기 쉬워 운영 난이도가 올라갑니다. 이 글은 Git submodule 서브모듈 실무 키워드에 맞춰 추가·업데이트·삭제, CI 설정, 흔한 오류, 모노레포 대안까지 정리합니다.
목차
- 개념: 슈퍼프로젝트와 gitlink
- 실전: 추가·클론·업데이트·삭제
- 고급: shallow·포크·대체 URL
- 비교: submodule vs subtree vs 패키지 vs 모노레포
- 실무 사례
- 트러블슈팅
- 마무리
개념: 슈퍼프로젝트와 gitlink
- 상위 저장소의
.gitmodules에 경로·URL·브랜치 추적 설정이 기록됩니다. - Git은 하위 디렉터리를 일반 파일이 아니라 gitlink(커밋 포인터)로 취급합니다.
- 따라서 “하위 저장소의 최신 main을 자동으로 따라간다”가 기본은 아닙니다. 상위에 기록된 SHA가 진실입니다.
실전: 추가·클론·업데이트·삭제
서브모듈 추가
기본 추가:
# 서브모듈 추가
git submodule add https://github.com/org/shared-lib.git vendor/shared-lib
# 상태 확인
git status
# Changes to be committed:
# new file: .gitmodules
# new file: vendor/shared-lib (커밋 SHA)
# 커밋
git commit -m "chore: add shared-lib submodule"
git push
특정 브랜치 추적:
# develop 브랜치 추적
git submodule add -b develop https://github.com/org/shared-lib.git vendor/shared-lib
# .gitmodules 확인
cat .gitmodules
# [submodule "vendor/shared-lib"]
# path = vendor/shared-lib
# url = https://github.com/org/shared-lib.git
# branch = develop
.gitmodules 파일 구조:
[submodule "vendor/shared-lib"]
path = vendor/shared-lib
url = https://github.com/org/shared-lib.git
branch = main
[submodule "vendor/utils"]
path = vendor/utils
url = https://github.com/org/utils.git
branch = stable
처음 클론할 때
방법 1: 클론 시 서브모듈 포함
# 모든 서브모듈 함께 클론
git clone --recurse-submodules https://github.com/org/main-app.git
# 또는 (동일)
git clone --recursive https://github.com/org/main-app.git
방법 2: 클론 후 서브모듈 초기화
# 먼저 메인 저장소 클론
git clone https://github.com/org/main-app.git
cd main-app
# 서브모듈 초기화 및 가져오기
git submodule init
git submodule update
# 또는 한 번에
git submodule update --init --recursive
방법 3: 병렬 클론 (빠름)
# 서브모듈을 병렬로 클론
git clone --recurse-submodules --jobs 4 https://github.com/org/main-app.git
서브모듈 상태 확인
# 서브모듈 목록 및 상태
git submodule status
# abc1234 vendor/shared-lib (v1.2.3)
# +def5678 vendor/utils (v2.0.0-5-gdef5678)
# -ghi9012 vendor/legacy (heads/main)
# 기호 의미:
# (공백): 정상 (상위가 가리키는 커밋과 일치)
# +: 서브모듈이 다른 커밋을 체크아웃 (변경됨)
# -: 서브모듈이 초기화되지 않음
# U: 병합 충돌
# 상세 정보
git submodule foreach 'echo $name: $(git rev-parse HEAD)'
하위 저장소를 최신으로 올리기
방법 1: 수동 업데이트
# 서브모듈 디렉터리로 이동
cd vendor/shared-lib
# 최신 코드 가져오기
git fetch origin
git checkout main
git pull origin main
# 상위 저장소로 돌아와서 변경 기록
cd ../..
git add vendor/shared-lib
git commit -m "chore: update shared-lib to v1.3.0"
git push
방법 2: 자동 업데이트
# 모든 서브모듈을 원격 브랜치 최신으로
git submodule update --remote
# 특정 서브모듈만
git submodule update --remote vendor/shared-lib
# 변경사항 커밋
git add .gitmodules vendor/shared-lib
git commit -m "chore: update submodules to latest"
방법 3: 특정 태그로 고정
cd vendor/shared-lib
git fetch --tags
git checkout v1.3.0
cd ../..
git add vendor/shared-lib
git commit -m "chore: pin shared-lib to v1.3.0"
팀원이 변경사항 받기
# 상위 저장소 업데이트
git pull
# 서브모듈도 업데이트
git submodule update --init --recursive
# 또는 한 번에
git pull --recurse-submodules
자동화 (Git 설정):
# pull 시 항상 서브모듈 업데이트
git config submodule.recurse true
# 이제 git pull만 해도 서브모듈 자동 업데이트
git pull
서브모듈 제거
완전 제거 절차:
# 1. 서브모듈 등록 해제
git submodule deinit -f vendor/shared-lib
# 2. Git에서 제거
git rm -f vendor/shared-lib
# 3. .git/modules 정리 (선택)
rm -rf .git/modules/vendor/shared-lib
# 4. 커밋
git commit -m "chore: remove shared-lib submodule"
git push
부분 제거 (나중에 다시 추가 가능):
# 등록만 해제 (디렉터리는 유지)
git submodule deinit vendor/shared-lib
# 다시 활성화
git submodule update --init vendor/shared-lib
고급: shallow·포크·대체 URL
CI에서 빠른 fetch
얕은 클론으로 시간 단축:
# 히스토리 1개만 가져오기
git submodule update --init --recursive --depth 1
# 또는 클론 시
git clone --recurse-submodules --shallow-submodules https://github.com/org/main-app.git
GitHub Actions 예제:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout with submodules
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
make build
GitLab CI 예제:
variables:
GIT_SUBMODULE_STRATEGY: recursive
build:
script:
- git submodule sync
- git submodule update --init --recursive --depth 1
- make build
Docker 빌드:
FROM node:18
WORKDIR /app
# Git 설치 (서브모듈 클론에 필요)
RUN apt-get update && apt-get install -y git
# 저장소 클론
COPY . .
# 서브모듈 초기화
RUN git submodule update --init --recursive --depth 1
# 빌드
RUN npm install && npm run build
CMD ["npm", "start"]
URL 재매핑 (엔터프라이즈 미러)
로컬 설정:
# 특정 서브모듈 URL 변경
git config submodule.vendor/shared-lib.url https://git.internal.corp/org/shared-lib.git
# 모든 GitHub URL을 내부 미러로
git config --global url."https://git.internal.corp/".insteadOf "https://github.com/"
SSH ↔ HTTPS 전환:
# HTTPS를 SSH로
git config --global url."[email protected]:".insteadOf "https://github.com/"
# SSH를 HTTPS로 (CI에서 유용)
git config --global url."https://github.com/".insteadOf "[email protected]:"
서브모듈에서 작업하기
브랜치 생성 및 푸시:
# 서브모듈로 이동
cd vendor/shared-lib
# 현재 상태 확인 (detached HEAD일 가능성 높음)
git status
# HEAD detached at abc1234
# 브랜치 생성
git checkout -b feature/my-change
# 작업 후 커밋
git add .
git commit -m "feat: add new feature"
# 푸시 (upstream 설정)
git push -u origin feature/my-change
# PR 생성 후 머지되면 상위 저장소 업데이트
cd ../..
git submodule update --remote vendor/shared-lib
git add vendor/shared-lib
git commit -m "chore: update shared-lib with new feature"
서브모듈 포크 사용
원본 대신 포크 사용:
# 1. 원본 서브모듈 제거
git submodule deinit -f vendor/shared-lib
git rm -f vendor/shared-lib
# 2. 포크 추가
git submodule add https://github.com/myorg/shared-lib.git vendor/shared-lib
# 3. 원본을 upstream으로 추가
cd vendor/shared-lib
git remote add upstream https://github.com/org/shared-lib.git
git fetch upstream
# 4. 원본 변경사항 가져오기
git merge upstream/main
git push origin main
cd ../..
git add vendor/shared-lib
git commit -m "chore: switch to forked shared-lib"
비교: submodule vs subtree vs 패키지 vs 모노레포
상세 비교표
| 항목 | Submodule | Subtree | 패키지 (npm/pip) | 모노레포 |
|---|---|---|---|---|
| 클론 복잡도 | 추가 명령 필요 | 단순 (git clone) | 단순 (패키지 설치) | 단순 |
| 버전 관리 | 커밋 SHA | 커밋 히스토리 | 시맨틱 버전 | 단일 버전 |
| 업데이트 | 수동 (명시적) | 수동 (git subtree pull) | 자동 (패키지 매니저) | 즉시 반영 |
| 독립 개발 | ✅ 쉬움 | ⚠️ 복잡 | ✅ 쉬움 | ❌ 어려움 |
| 권한 관리 | 레포별 분리 | 레포별 분리 | 레지스트리 권한 | 단일 레포 |
| CI 복잡도 | 높음 | 중간 | 낮음 | 낮음 |
| 학습 곡선 | 가파름 | 가파름 | 낮음 | 중간 |
Submodule 예제
# 구조
main-app/
├── src/
├── vendor/
│ ├── shared-lib/ (서브모듈)
│ └── utils/ (서브모듈)
└── .gitmodules
# 클론
git clone --recurse-submodules https://github.com/org/main-app.git
# 업데이트
cd vendor/shared-lib
git pull origin main
cd ../..
git add vendor/shared-lib
git commit -m "update"
Subtree 예제
# 추가
git subtree add --prefix=vendor/shared-lib https://github.com/org/shared-lib.git main --squash
# 구조 (일반 디렉터리처럼 보임)
main-app/
├── src/
└── vendor/
└── shared-lib/ (일반 디렉터리, 히스토리 포함)
# 업데이트
git subtree pull --prefix=vendor/shared-lib https://github.com/org/shared-lib.git main --squash
# 변경사항 업스트림에 푸시
git subtree push --prefix=vendor/shared-lib https://github.com/org/shared-lib.git feature-branch
패키지 예제 (npm)
# 구조
main-app/
├── src/
├── node_modules/
│ └── @org/shared-lib/ (npm 패키지)
└── package.json
# 설치
npm install @org/shared-lib
# 업데이트
npm update @org/shared-lib
# package.json
{
"dependencies": {
"@org/shared-lib": "^1.2.3"
}
}
모노레포 예제 (Turborepo)
# 구조
monorepo/
├── apps/
│ ├── web/
│ └── api/
├── packages/
│ ├── shared-lib/
│ └── utils/
└── turbo.json
# 클론 (한 번에 모든 것)
git clone https://github.com/org/monorepo.git
# 빌드 (의존성 자동 해결)
npm install
npm run build
# 변경사항 (원자적 커밋)
git add packages/shared-lib apps/web
git commit -m "feat: update shared-lib and use in web"
선택 가이드
Submodule 선택:
- ✅ 독립적인 릴리스 주기
- ✅ 레포별 권한 분리 필요
- ✅ 여러 프로젝트에서 동일 라이브러리 사용
- ❌ 팀이 Git 초보
패키지 선택:
- ✅ 버전 관리 중요
- ✅ 퍼블릭 또는 프라이빗 레지스트리 사용
- ✅ 자동 업데이트 원함
- ❌ 소스 코드 직접 수정 필요
모노레포 선택:
- ✅ 여러 패키지 동시 수정 빈번
- ✅ 원자적 변경 중요
- ✅ 공통 CI/CD 파이프라인
- ❌ 레포 크기 제한 (수십 GB)
실무 사례
1. 공유 프로토콜 버퍼 레포
구조:
api-gateway/
├── src/
└── proto/ (서브모듈 → shared-proto-repo)
user-service/
├── src/
└── proto/ (서브모듈 → shared-proto-repo)
order-service/
├── src/
└── proto/ (서브모듈 → shared-proto-repo)
워크플로우:
# 1. shared-proto-repo에서 스키마 변경
cd shared-proto-repo
git checkout -b feature/add-user-field
# user.proto 수정
git commit -m "feat: add email field to User"
git push origin feature/add-user-field
# 2. PR 머지 후 각 서비스 업데이트
cd api-gateway
git submodule update --remote proto
git add proto
git commit -m "chore: update proto to include email field"
2. 공유 UI 컴포넌트 라이브러리
구조:
web-app/
├── src/
└── components/ (서브모듈 → shared-components)
admin-app/
├── src/
└── components/ (서브모듈 → shared-components)
버전 고정 전략:
# 안정 버전 사용 (태그)
cd components
git fetch --tags
git checkout v2.1.0
cd ..
git add components
git commit -m "chore: pin components to v2.1.0"
# 최신 개발 버전 사용 (브랜치)
cd components
git checkout develop
git pull origin develop
cd ..
git add components
git commit -m "chore: update components to latest develop"
3. 문서 레포 분리
구조:
website/
├── src/
├── public/
└── docs/ (서브모듈 → documentation-repo)
자동 업데이트 (GitHub Actions):
name: Update Docs
on:
repository_dispatch:
types: [docs-updated]
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.PAT }}
- name: Update docs submodule
run: |
git submodule update --remote docs
git config user.name "GitHub Actions"
git config user.email "[email protected]"
git add docs
git commit -m "chore: update docs" || exit 0
git push
4. CI 캐싱 전략
GitHub Actions:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Cache submodules
uses: actions/cache@v3
with:
path: |
.git/modules
vendor/shared-lib
key: submodules-${{ hashFiles('.gitmodules') }}-${{ hashFiles('vendor/shared-lib/.git/HEAD') }}
- name: Build
run: make build
GitLab CI:
build:
cache:
key:
files:
- .gitmodules
paths:
- .git/modules
- vendor/
script:
- git submodule sync
- git submodule update --init --recursive
- make build
트러블슈팅
| 증상 | 원인 | 대응 |
|---|---|---|
| 빈 디렉터리 | submodule 미초기화 | submodule update --init --recursive |
| detached HEAD | submodule은 포인터만 따라감 | 작업 시 브랜치 생성·명시적 push |
| 권한 오류 | CI 토큰에 서브레포 접근 없음 | machine user·deploy key·조직 SSO |
| 충돌 merge | 상위·하위 동시 변경 | 하위에서 먼저 정리 후 상위 SHA 갱신 |
| 버전 불일치 | 일부만 pull | 문서에 “항상 이 명령” 고정 |
“서브모듈 안에서 실수로 push” 방지: 하위 저장소에 branch protection을 두고, 상위 bump는 PR로만 받습니다.
마무리
Git submodule은 멀티레포 의존성을 Git 네이티브로 표현하는 도구이며, 성공하려면 클론·업데이트·CI를 레포 README에 한 줄이라도 표준 명령으로 박아 두는 것이 중요합니다. 원격 협업과 Actions 패턴은 Git push pull·원격 협업, 충돌 해결 사고례는 Git merge conflict 실전과 함께 보면 좋습니다.