배경
AWS 환경에서 운영시 사내 nexus를 사용할 수 없어서, common 모듈 같은 공통 모듈을 별도 프로젝트로 관리하고, git submodule로 포함시킨다는데 이게 어떤 의미일까 ?
- 보통 사내에서 빌드된 공통 라이브러리(common module)는 Nexus (또는 Artifactory) 같은 사설 Maven Repository에 업로드
- 다른 프로젝트에서는 의존성으로 이렇게 불러온다.
1
2
3
dependencies {
implementation 'com.company:common:1.2.3'
}
- 이렇게 하면 빌드 시 Gradle/Maven이 Nexus에서 jar를 받아온다.
- 그런데 AWS 환경에서는 종종 다음과 같은 이유로 사내 Nexus에 접근이 불가능할 수 있다:
- AWS VPC 내부망에서 사내망(Nexus 서버) 으로 직접 접근이 차단됨
- VPN 연결 없이 외부망(AWS)에서 내부망(Nexus)에 접근 불가
- Nexus 서버가 프라이빗 네트워크에 있고, 인터넷에 노출되지 않음
- 즉, 의존성 아티팩트를 원격 Repository에서 받을 수 없는 상황이 된다.
※ Nexus ?
Sonatype Nexus Repository는 빌드 결과물(예: jar, war, npm 패키지, Docker 이미지 등)을 중앙에서 저장하고 배포할 수 있게 해주는 Artifact Repository(아티팩트 저장소) 즉, Maven Central이나 npmjs.org 같은 공용 저장소의 사내 버전
- 필요성
- 여러 프로젝트에서 공통 라이브러리(core, util 등) 사용
- 외부 네트워크 접근이 제한된 환경 (AWS VPC, 내부망 등)
- 회사 내부 전용 라이브러리 (보안상 외부에 공개 불가)
- 그래서 “사내 전용 Maven Central” 같은 서버가 필요
| 역할 | 설명 |
|---|---|
| Artifact 저장소 | 빌드 결과물(JAR, WAR, ZIP, Docker 이미지 등)을 업로드(Publish)하고 다른 프로젝트에서 다운로드(Fetch) 가능 |
| 사설 Repository 관리 | 외부 공개 불가능한 내부용 라이브러리를 보관 |
| Proxy Cache | Maven Central, npm registry 등의 캐시 프록시 역할 (외부 패키지를 한 번 받아두면 내부에서 빠르게 재사용 가능) |
| 버전 관리 | 아티팩트의 버전을 1.0.0, 1.0.1, SNAPSHOT 등으로 관리 |
| 권한 관리 | LDAP, SSO 연동으로 팀별 접근 권한 제어 가능 |
- AWS 환경에서는 Nexus 대신 AWS CodeArtifact(AWS 관리형 Nexus 같은 서비스)가 같은 역할을 한다.
Git Submodule
하나의 Git 저장소 안에 다른 Git 저장소를 하위 디렉터리로 포함시키는 기능
1
2
3
4
main-repo/
├── .gitmodules
├── app/
└── common-lib/ ← 다른 Git 저장소 (submodule)
주요 특징
| 항목 | 설명 |
|---|---|
| 독립 버전 관리 | Submodule은 자체 Git 히스토리를 가지고 있어요. 부모 저장소와 별도로 커밋, 푸시 가능. |
| 특정 커밋 고정 | 부모 저장소는 submodule의 “브랜치”가 아니라 “특정 커밋 SHA”를 참조합니다. |
| 업데이트는 수동 | submodule 쪽에서 변경이 있어도, 부모 저장소에서는 git submodule update로 직접 갱신해야 반영됩니다. |
.gitmodules 파일 | submodule 경로와 원격 저장소 URL을 관리하는 설정 파일입니다. |
멀티모듈 프로젝트와의 차이
| 비교 항목 | Git Submodule | 멀티모듈 프로젝트 (예: Gradle, Maven 등) |
|---|---|---|
| 소스 관리 단위 | 각 모듈이 서로 다른 Git 저장소 | 하나의 Git 저장소 안에 여러 모듈 |
| 버전 관리 | 각 모듈은 자체 버전, 커밋 히스토리를 가짐 | 전체가 한 커밋 단위로 버전 관리됨 |
| 의존성 연결 방식 | Git 커밋 기반 링크 (.gitmodules) | 빌드 시스템(Gradle, Maven)의 settings.gradle이나 pom.xml로 연결 |
| 업데이트 방식 | 수동으로 git submodule update | 자동으로 같은 브랜치/커밋 기준에서 빌드 |
| 빌드 통합성 | 독립적 빌드, 통합 자동화 어려움 | 전체 프로젝트를 한 번에 빌드 가능 |
| 주 사용 목적 | 외부 공용 라이브러리나 공통 모듈 재사용 | 대규모 단일 프로젝트를 논리적으로 분리 |
장/단점
| 구분 | 내용 |
|---|---|
| 장점 | - Nexus가 없어도 공통 모듈 사용 가능 - 각 모듈의 버전을 Git 커밋으로 고정 가능 - 별도 jar 배포 없이 코드 동기화 가능 |
| 단점 | - submodule 업데이트를 수동으로 해야 함 (git submodule update)- 여러 프로젝트가 같은 submodule을 쓰면 버전 동기화가 번거로울 수 있음 - 빌드 시스템 통합 관리가 어려움 |
적용
- 디렉토리 구조
1
2
3
4
5
6
repo1/
├── .gitmodules
├── api/
├── admin/
├── web/
└── core/ ← submodule (repo2)
- 명령어
1
2
3
4
5
6
7
# repo1에서 core 모듈(submodule) 추가
# 즉, repo1은 core 저장소의 특정 커밋 SHA를 기록 (.gitmodules 파일과 내부 .git/config에 저장됨)
git submodule add https://github.com/company/repo2.git core
# 서브모듈 초기화 및 업데이트
git submodule init
git submodule update
repo1/settings.gradle
1
2
include(":api", ":admin", ":web", ":core")
project(":core").projectDir = file("core")
repo1/build.gradle
1
2
3
dependencies {
implementation project(":core")
}
core 코드는 같은 워크스페이스에 존재하므로, AWS 환경에서도 별도 Nexus 없이 빌드 가능해진다.
- 이때 common은 별도의 Git 레포지토리이며, main-project 안에는 그 레포지토리의 특정 커밋을 submodule로 연결
- 빌드 시 common의 소스 코드가 로컬에 그대로 존재하므로, AWS 환경에서도 네트워크를 통한 Nexus 접근 없이 바로 빌드 가능.
- 즉, common 코드를 라이브러리처럼 직접 포함시키는 형태가 되는 것
main-project/settings.gradle:
1
2
include(":app", ":common")
project(":common").projectDir = file("common")
실습
submodule을 참조하는 repo : repo1
submodule repo : repo2
repo2에서 반영한 최신 내역 repo1에서 가져오기
repo1에서는 foo3 호출중

repo2에서는 foo4로 메서드명 변경해서 반영한 상태

repo1에서 repo2의 변경사항 반영하기
1 2
1. git submodule update --remote common 2. push


repo1에서 repo2 코드 변경하고 반영하기
1
2
3
4
5
1. 코드 수정
2. cd common
3. git add .
4. git commit -m "foo5 반영"
5. git push origin master

- 지금 오류 상황은 submodule을 detached HEAD 상태에서 작업했기 때문에 발생
- HEAD : 현재 체크아웃된 브랜치 또는 커밋을 가리키는 포인터
- detached HEAD : 브랜치가 아닌 특정 커밋 위에서 작업하는 상태
- Git은 항상
HEAD → 브랜치 → 커밋순으로 연결되어 있어야 한다.- ex :
HEAD → master → (a1b2c3) - 하지만 특정 브랜치가 아니라, 특정 커밋(SHA)을 직접 checkout하면 이렇게 됨:
HEAD → (a1b2c3)- master 같은 브랜치에 연결되지 않음 즉, HEAD가 “분리(detached)”된 상태.
- ex :
- foo5 반영 커밋(
a33f7410)은 아직 사라지지 않았고, 브랜치에서 연결되지 않은 “떠 있는 커밋” 상태로 남아 있다.- Git에서는 이런 커밋을 dangling commit이라고 한다.
git fsck --lost-found: dangling commit 확인

- 즉,
a33f7410커밋은 브랜치(master)가 아닌, detached HEAD에서 만든 커밋이었기 때문에 master 브랜치로 이동하면서 브랜치 히스토리에서 보이지 않게 된 것. (dangling commit) - dangling commit을 submodule master에 붙이고 싶으면
git checkout mastergit cherry-pick a33f7410git push origin master

submodule이 detached HEAD 되는 이유
- 서브모듈은 항상 “커밋 SHA”를 기준으로 체크아웃됨.
- 즉, 브랜치를 checkout 한 게 아니라 SHA를 checkout 함 → detached HEAD
- 여기서 작업 후 커밋하면 “임시 커밋”이 됨
- 다시 master checkout 하니까 foo5 커밋은 잃어버린 것처럼 보임
- 그러니 push 시도해도 안 됐던 것
submodule 코드 수정할 때 올바른 절차
1
2
3
4
5
6
7
1. git submodule update --remote
2. cd common
3. git switch master # 이제 HEAD가 정상 브랜치에 연결됨: HEAD → master
4. 코드 수정
5. git add .
6. git commit -m "message"
7. git push



