Git subtree를 활용한 코드 공유
서비스를 개발하다 보면 코드를 공유해야 하는 일이 생긴다. 백엔드를 마이크로 서비스 구조로 개발하면 API 리스펀스, 각종 데이터 모델에 같은 타입을 사용해서 일관성을 유지해야 한다. 그리고 프론트엔드 웹을 데스크탑, 모바일로 분리해서 개발한다면 데이터 모델뿐만 아니라 컴포넌트 및 다양한 유틸리티 모듈을 함께 사용할 필요가 생긴다. 만약 백엔드가 node.js로 만들어져 프론트엔드 웹과 Javascript라는 같은 언어를 사용한다면 코드 공유의 필요성은 더욱 클 것이다.
코드 공유를 위한 방법은 여러 가지가 있다. NPM private 레지스트리, Bit, Git submodule, 그리고 전통적이고 간단한 “복붙”(copy and paste)같은 방법이 있다. 이 글에서는 Git subtree로 코드를 공유하는 방법을 소개한다.
Git Submodule의 단점
Git Submodule(이하 서브모듈)은 Git 저장소에 다른 Git 저장소를 추가하는 방법이다. 서브모듈로 추가된 저장소(repository)는 상위 저장소에서 1개의 폴더로 취급되며 일반적인 파일처럼 접근해서 사용할 수 있다.
그런데 문제가 있다. 상위 저장소는 서브모듈의 정보 중에서 체크아웃한 커밋의 SHA 값만 기록한다. 그런데 만약 서브모듈의 원격에 새로운 내용이 푸시되어서 서브모듈을 업데이트해야 하는 상황이 생긴다면 어떻게 될까? 아래와 같은 시나리오를 가정해보자.
- 서브모듈 B를 추가한 저장소 A에서 서브모듈을 직접 수정한 후 커밋을 했다.
-
서브모듈 B의 원격에 새로운 내용이 푸시되었다.
- 저장소 A에 있는 서브모듈의 내용과 원격에 있는 서브모듈의 내용은 서로 다르다. 충돌이 발생할 수 있다.
- 서브모듈의 업데이트를 해야 한다.
git submodule update
명령어로 사용한다.
위와 같은 시나리오에서 서브모듈의 병합의 제대로 이뤄질까? 대답은 “아니오”다. 왜냐면 상위 저장소에서 서브모듈을 SHA 값, 하나의 바이너리처럼 취급하기 때문이다. SHA 값이 다르므로 그냥 최신 커밋의 내용으로 교체해 버린다. 저장소 A에서 작업한 중요한 변경사항은 그냥 사라져 버릴 것이다. 만약 병합을 제대로 하고 싶다면 사용자가 직접 서브모듈에 새로운 브랜치를 만들어서 커밋하고. 서브모듈 업데이트를 한 후 직접 병합하고 충돌을 해결한 후 푸시해야 한다.
그러면 서브모듈로 추가한 소스는 건드리지 않는다는 원칙을 세우고 작업을 하면 되지 않나? 라고 생각할 수도 있다. 하지만 작업을 하다 보면 이것이 서브모듈인지 상위 저장소의 소스인지 헷갈릴 수 있어서 실수로 수정하고 커밋을 할 가능성은 얼마든지 있다.
Git Subtree
Git Subtree(이하 서브트리)는 SHA 값만 저장하는 서브모듈과 달리 상위 저장소에 파일을 직접 추가하고 트래킹한다. 자연히 서브트리의 파일 및 변경사항도 상위 저장소에 기록된다. 그리고 서브트리의 원격에 있는 소스와 서브트리를 추가한 저장소의 소스가 서로 달라도 “subtree merge” 기능을 사용해서 양쪽의 변경사항을 모두 반영할 수 있다.
이처럼 상위 저장소에서 서브트리를 직접 수정하고 원격에 푸시할 수 있다는 것이 서브모듈과 큰 차이점이다. 예를 들어 서브트리의 컴포넌트를 상위 저장소의 프로젝트에서 사용할 때 버그를 발견했다면, 즉시 수정해서 커밋한 후 버그를 해결한 상태로 서브트리의 원격에 반영할 수 있다는 편리함이 있다.
물론 단점도 있다. 서브트리를 추가한 모든 사용자가 서브트리의 내용을 자유롭게 변경해서 원격에 푸시할 수도 있다는 점이다. 프로젝트를 공동으로 진행할 때 어떤 사람이 작업한 feature A 브랜치에서 작동하는 서브트리의 소스를 푸시했는데, 그것을 내려받은 다른 사람의 feature B 브랜치에서는 오류가 발생할 수도 있다. 소규모 팀에서 작업할 때는 쉽게 대응이 가능하겠지만, 만약 수십 명이 프로젝트에 참여하는 상황이라면 위와 같은 문제가 발생할 가능성이 더 커지고 커뮤니케이션도 원활히 되지 않을 수 있다. 대규모 팀이라면 자유도가 높은 서브트리의 사용보다 프라이빗 NPM 레지스트리를 사용하면서 공용 소스 코드의 수정에 엄격한 기준을 세우는 등 다른 방법을 사용하는 편이 나을 수도 있다.
Git 서브트리 사용법
이번 글을 위해 예제를 위한 샘플 저장소를 생성한 후 진행했다.
- 서브트리를 사용할 상위 저장소: rhostem/gitsubtree-consumer
- 서브트리로 추가될 저장소: rhostem/gitsubtree-lib
1. 서브트리로 사용할 원격 저장소 추가
다른 저장소를 서브트리로 바로 추가할 수도 있다. 하지만 서브트리를 fetch, pull, push도 할 것이므로 서브트리로 사용할 Git 저장소를 상위 저장소의 새로운 원격 저장소로서 추가한다.
git remote add gitsubtree-lib git@github.com:rhostem/gitsubtree-lib.git
git remote add <원격 저장소의 이름> <원격 저장소의 주소>
원격 저장소의 이름으로 subtree
를 사용했다. 기본 저장소 외에 하나가 더 추가되었음을 확인할 수 있다.
git remote ⮐
origin
gitsubtree-lib
2. 새로운 원격 저장소의 브랜치를 서브트리로 추가.
git subtree add --prefix lib gitsubtree-lib master
git subtree add --prefix <클론할 폴더> <원격 저장소의 이름> <브랜치 이름>
--prefix
옵션으로 서브트리를 클론할 폴더를 지정한다. 그리고 원격 저장소의 이름, 체크아웃할 브랜치 이름을 지정하면 서브트리로 gitsubtree-lib 저장소가 추가된다. 브랜치는 master
를 사용하도록 했다.
서브트리로 추가한 직후 폴더 구조는 아래와 같다.
.
├── README.md (gitsubtree-consumer의 파일)
└── lib
└── README.md (gitsubtree-lib의 파일)
3. 서브트리를 원격에서 내려받기(pull)
서브트리의 원격에 올라온 커밋을 내려받는다. 서브트리를 추가하는 명령어와 큰 차이가 없으며, 자동으로 병합(merge)이 진행된다.
git subtree pull --prefix lib gitsubtree-lib master
커맨드라인에서 실행 후 병합이 완료되면 에디터가 열리면서 커밋 메시지의 저장을 요구할 수도 있다.
4. 서브트리를 원격에 올리기(push)
상위 저장소에서 서브트리의 소스를 직접 수정 후 변경사항을 커밋하고 원격에 푸시할 수 있다. 역시 명령어는 내려받기와 큰 차이가 없다.
git subtree push --prefix lib gitsubtree-lib master
명령어가 짧지 않으므로 쉘 스크립트나 NPM 스크립트로 작성해서 사용하는 편이 좋다. 그리고 서브트리 명령어는 항상 상위 저장소의 최상위 폴더에서 실행해야 한다. 서브트리의 소스가 있는 폴더에서 git subtree
명령어를 사용하면 동작하지 않는다.