git subtree를 사용하여 재사용할 코드 독립 시키기

안녕하세요. SK 플래닛 Commerce Data PF 개발팀의 김경민 매니저 입니다. 현재 Java, Spring을 사용하여 백엔드 서버 어플리케이션 개발을 하고 있습니다. 이번 포스팅을 통해 git subtree를 사용한 재사용할 코드에 대해 고려했던 부분에 대해 말씀드리려고 합니다.

들어가기 전에

이 문서는 git에 대한 내용을 포함하고 있습니다. 내용을 이해하기 위해서 git에 대한 이해와 사용 경험이 필요합니다.

프로젝트 분리 이슈

최근에 참여한 프로젝트에서 시스템 간 전문을 사용하여 연동하는 모듈을 개발했습니다. 미리 정의된 다수의 전문을 주고 받으면서 그에 해당하는 업무 로직을 적용하여 처리하는 어플리케이션 이었습니다. 이를 구현하기 위해 전문을 정의할 수 있는 기반 기능을 먼저 개발하고 실제 사용하는 전문과 각각의 전문을 업무에 따라 처리하는 프로세서를 구현하였습니다. 소스 코드 관리는 git으로 했습니다.
개발을 완료하고 나서 전문 정의 및 처리 엔진은 전문을 사용하는 다른 프로젝트에서 충분히 재사용이 가능할 것이라는 생각이 들었습니다. 그래서 프로젝트 종료 후 전문 공통 기능을 분리하기로 결정하고 작업에 들어갔습니다.

목표 이미지

분리 작업에 들어가기 전의 프로젝트 구성입니다.

프로젝트 구성 - 분리 전그림 1. 프로젝트 구성 – 분리 전

message-adapter가 분리할 대상인 전문 처리 모듈입니다. message-adapter는 em-core에 의존하고 있습니다. em-core는 업무 도메인을 정의해 놓은 공통 모듈로 프로젝트 내의 다른 모듈에서도 사용하고 있습니다.

프로젝트 구성 - 분리 후그림 2. 프로젝트 구성 – 분리 후

분리 작업 후의 목표 구성입니다. message-adapter에서 분리할 대상을 떼어내어 message-common 디렉토리로 옮깁니다. message-common은 재사용할 대상이므로 독립적으로 존재하는 라이브러리 형태가 되길 원하기 때문에 내부의 다른 모듈에 의존하지 않게 합니다. 반면, message-adapter는 message-common에 의존하게 됩니다. 분리한 message-common은 새로운 저장소(Repository)와 연결합니다.
여기서 중요한 것은 message-common이 새로 생성한 저장소뿐만 아니라 기존 저장소에도 동시에 존재할 수 있게 하는 것입니다. 이렇게 하고 싶었던 이유는 두 가지가 있습니다.
첫째, 코드 수정에 대한 테스트 및 적용을 신속하게 하고 싶었습니다. 오픈한 지 얼마 안된 프로젝트이기 때문에 코드가 수정될 가능성이 큽니다. 별도의 프로젝트로 분리하여 관리할 경우 message-common에 수정사항이 발생하면 수정사항을 반영하여 빌드한 jar 파일을 message-adapter에서 다시 가져간 후 테스트를 진행해야 합니다. 과정이 번거롭고 배포 자동화를 하더라도 시간이 어느 정도 소요됩니다.
둘째, 원격 저장소를 분리하는 이유는 추후 관리를 위해서 입니다. message-common은 현재 프로젝트와 별개로 독립적으로 존재할 것이며 지속적으로 개선해 나갈 것이기 때문입니다.

다시 설명하면, message-common은 기존 프로젝트 내에 속한 형태에서 수정이 될 수 있으면서, 프로젝트와 별개로 독립적인 환경에서 또한 수정이 될 수 있습니다. 그리고 이 2개의 별도 환경에서 수정된 이력은 모두 하나의 원격 저장소에 반영이 되야 합니다.

message-common 분리 후 환경그림 3. message-common 분리 후 환경

해결 방법

git을 사용하기 때문에 git에 제가 원하는 기능이 있는지 찾아보았습니다. 다행히 저의 요구사항에 맞는 기능이 2개나 있었습니다. 그것은 바로 subtree와 submodule 입니다. 2개의 기능을 비교 및 검증해보고 subtree를 사용하기로 결정했습니다. 왜 submodule 대신에 subtree를 선택했는지는 이후 내용에서 설명 드리겠습니다. subtree를 선택했으니 기능에 대해 알아보도록 하겠습니다.

Subtree

git은 merge 명령어를 사용하여 2개의 브랜치를 합치는 기능을 제공합니다. 이 때 2개의 브랜치의 root 디렉토리를 기준으로 하위의 내용을 합치게 됩니다.

그림 4. git merge 전

git merge 완료 후그림 5. git merge 완료 후

이것이 일반적으로 git merge가 행해지는 방법입니다. (참고로 git은 기본적으로 recursive merge strategy를 이용하여 merge 작업을 수행합니다.)
그런데 때로는 하위 디렉토리에 특정 브랜치의 코드를 통째로 가지고 오고 싶은 경우가 생길 수 있습니다. 예를 들어, A라는 브랜치와 B 브랜치를 merge하는데 B 브랜치를 A의 하위 디렉토리에 그대로 merge하고 싶은 경우가 생길 수 있습니다.

하위 디렉토리 merge 전그림 6. 하위 디렉토리  merge 전

하위 디렉토리 merge 완료 후그림 7. 하위 디렉토리 merge 완료 후

이런 경우에 사용하는 merge 전략을 subtree merge strategy라고 합니다. git은 subtree라는 명령어를 제공하여 하위 디렉토리로의 merge 기능을 제공합니다.

예시를 통해서 subtree의 기능을 상세히 설명하겠습니다.
야구 게임을 개발하는 bb_game과 물리엔진을 구현하는 physics라는 프로젝트가 있다고 가정해보겠습니다. 각각은 별도의 독립적인 프로젝트이며 git으로 역시 독립적인 저장소에서 관리되고 있습니다.
각각 프로젝트를 진행하다 야구 게임에 물리엔진을 적용할 시점이 되었습니다. 그런데 physics 프로젝트는 아직 개발이 진행되고 있습니다. 개발 팀은 bb_game과 physics를 동시에 진행하기로 결정하고 작업의 편의를 위해서 2개를 하나의 프로젝트로 합치려고 합니다.

우선, bb_game 프로젝트에 physics의 원격 저장소를 추가합니다.

subtree add 명령어를 이용하여 physics를 bb_game의 하위 디렉토리에 추가합니다.

prefix 옵션은 추가할 프로젝트가 위치하는 디렉토리 경로를 의미합니다. 위의 예는 bb_game 프로젝트 하위의 physics 디렉토리에 해당 프로젝트 소스 코드를 가져오는 명령 구문입니다.

physics에 대한 브랜치를 추가하겠습니다. 로컬에 physics라는 이름의 브랜치를 추가하고 physics의 원격 저장소의 develop 브랜치를 가리키도록 합니다. 이렇게 하면 로컬에서 subtree 프로젝트에 대한 추적이 용이하게 됩니다.

이로써 하나의 프로젝트에 서로 다른 원격 저장소를 가지는 2개의 브랜치가 생성되었습니다. 각각의 브랜치에서 소스 코드를 확인하면 다음과 같은 차이점이 있습니다.

개인적인 의견으로, subtree는 특정 디렉토리를 root 디렉토리로 가지는 브랜치를 생성할 수 있고 별도의 원격 저장소에 연결할 수 있게 하는 기능을 제공한다고 이해할 수 있을 것 같습니다. 이러한 기능은 하위 프로젝트에 대한 관리를 가능하게 합니다.

이제 bb_game 프로젝트에 physics 프로젝트 소스를 추가하여 동시에 관리할 수 있게 되었습니다. physics 디렉토리 내의 수정사항도 bb_game 브랜치의 이력에 반영될 것입니다. 반면, physics의 수정 내용을 physics 원격 저장소에 반영할 필요가 있습니다. 즉, 가져온 프로젝트의 변경 내용을 원래 프로젝트에 돌려주는 것입니다. 이런 경우 subtree push를 사용합니다.

반대로, 원격 저장소의 수정된 내용을 현재 프로젝트에 반영하고 싶은 경우가 생길 수 있습니다. 이 경우는 git pull을 사용합니다. 외부에서 변경된 physics 프로젝트의 변경 내용이 bb_game 브랜치에 merge 됩니다.

현재 프로젝트의 일부를 독립시키기

처음에 하나의 프로젝트로 진행하다가 그 중 일부를 분리시켜야 할 경우가 있습니다. (앞에서 소개한 프로젝트 사례가 바로 그렇습니다.) 이런 경우 subtree split을 사용하여 분리할 수 있습니다.

–prefix 옵션에 분리할 대상 경로를 지정합니다. 여기서는 physics 디렉토리를 지정하여 그 안의 내용을 분리할 대상으로 지정했습니다. -b 옵션을 사용하여 분리할 대상의 브랜치 이름을 정합니다. 이렇게 하면 physics라는 브랜치가 생기고 그 root 디렉토리는 bb_game/physics/가 됩니다.

다음으로 subtree 브랜치의 내용을 원격 저장소에 push합니다.

이후부터는 위의 설명과 같습니다. subtree의 내용을 원격에 반영할 때는 git subtree push를, subtree 원격 저장소의 변경 내용을 로컬로 가져오고 싶을 때는 git subtree pull을 사용하면 됩니다.

subtree 장점

사실 git에는 하위 프로젝트(서브 프로젝트)의 관리에 대한 방법이 subtree 외에 submodule이란 기능도 있습니다. submodule과 subtree는 동일한 성격의 기능을 제공하고 있습니다. git subtree 매뉴얼에도 subtree는 submodule과 동일한 작업에 사용된다고 나와있습니다.
그렇다면 제가 왜 submodule 대신 subtree를 선택했을까요?
subtree는 추가적인 git 설정 파일이 존재하지 않습니다. 반대로 submodule은 추가적인 git 설정 파일이 필요합니다. (추가 설정 파일은 .gitmodule 디렉토리 하위에 생성됩니다.) 추가 설정 파일이 없다는 것은 큰 장점을 가집니다. 진행 중인 프로젝트에 서브 프로젝트를 만들기 위해 설정 파일을 만든다면, 이에 대한 관리를 지속적으로 해줘야 될 것입니다. 즉, 항상 추가 설정 파일이 있다는 것을 염두에 두고 작업을 해야 되기 때문에 번거롭습니다.
또한 subtree를 사용하면 모든 작업 인원이 subtree의 존재를 알 필요가 없습니다. (반면, submodule에서는 모든 작업 인원이 submodule에 대해 인지해야 합니다.) 이게 무슨 뜻이냐면, subtree에 해당하는 부분을 담당하지 않는 개발자는 subtree를 사용할 필요 없이 상위 브랜치에서만 작업을 하면 됩니다. 모든 변경 이력은 상위 브랜치에 기록되며 그 중에서 subtree 내의 변경 이력이 subtree 브랜치에 중복해서 기록됩니다. 그러므로 subtree 내의 작업에 관여하는 인원만 subtree push와 subtree pull을 이용하여 subtree 원격 저장소에 반영을 하면 되고 그 외의(subtree 내의 작업과 관련없는) 인원들은 해당 디렉토리가 subtree인지 아닌지는 몰라도 된다는 뜻입니다.

subtree 단점

원격 저장소에 subtree push를 하는 경우 이력이 중복해서 남겨집니다. subtree push가 상위 프로젝트의 이력 중에 서브 프로젝트에 해당하는 이력들만 모아서 별도의 브랜치를 만드는 것이기 때문입니다.

그림 8. 중복된 이력 생성

–squash 옵션을 사용하여 이력을 축약시킬 수 있지만 근본적인 해결책은 되지 않습니다. 사실 서브 프로젝트에서 코드 변경을 한 다음 subtree pull로 가져오는 것이 이력을 더 깔끔하게 관리할 수 있는 방법입니다. 하지만 상위 프로젝트 내에서 변경을 하는 이점이 크기 때문에 이력의 깔끔함이냐 작업의 용이함이냐 사이에서 하나를 선택해야 할 것입니다.

또한, 여러 명이 subtree를 사용할 때 주의할 점이 있습니다. 바로 subtree push/pull의 중복 동시 적용 문제입니다. subtree가 포함된 프로젝트에 A, B 두 개발자가 참여하고 있다고 가정해 보겠습니다. 두 개발자가 동일한 이력에서 작업하다가 하위 프로젝트의 원격에서 코드를 가져올 일이 생겼습니다. 그래서 A, B 둘 다 각자 로컬에서 git subtree pull을 수행했습니다. 동일한 변경 이력이 각각의 로컬에 반영이 됩니다. 그런 후에 A가 git push로 origin 원격 저장소에 변경 내용을 반영한 후, 곧바로 B 또한 git push를 하게 되면 동일한 내용의 이력을 중복해서 merge해야 하는 상황이 발생합니다. subtree pull 또한 마찬가지입니다.
이렇게 subtree 디렉토리 내용을 여러 명이 동시에 수정하는 경우 담당자를 정해서 subtree pull/push의 작업을 전담하도록 하는 것이 좋습니다.

결론

개발을 하다 보면 어떤 기능은 나중에 다른 곳에서도 사용할 수 있겠다고 느낄 때가 있습니다. 하지만 개발 도중 당장 프로젝트를 분리하여 별도로 개발하기는 사실상 어려울 때가 많습니다. 프로젝트가 종료된 후에 새로운 프로젝트를 만들어 해당 기능들을 copy and paste하여 분리할 수도 있지만 소스 코드가 이원화되기 때문에 새로 생성한 프로젝트의 소스를 원래 프로젝트에 다시 반영하고 싶은 경우에 그 과정이 번거롭습니다.
이렇게 코드 분리 및 재사용에 대한 이슈가 있을 때 subtree를 사용하면 문제를 쉽게 해결할 수 있습니다. 사용법이 어렵지 않으며 기존 프로젝트에 미치는 영향도가 거의 없는 것은 큰 장점입니다. git submodule도 대안이 될 수 있긴 하지만 간편하고 쉽게 사용하기에 subtree가 좋다고 생각합니다. 특히 프로젝트 중간에 또는 완료 시점에 일부 기능을 별도의 프로젝트로 분리해내는데 가장 좋은 방법이라고 생각이 듭니다. 물론 본연의 기능인 서브 프로젝트 관리에도 충실합니다.
앞으로 진행할 개발 프로젝트에 자주 사용해보려고 합니다. 저와 같은 이슈가 있으신 분들은 적극적으로 검토를 해봐도 괜찮을 거라고 생각합니다.

References

subtree 매뉴얼
subtree 가이드
subtree tutorial
subtree vs submodule
submodule tutorial

안녕하세요. SK 플래닛의 김경민 매니저 입니다. 주로 백엔드 개발을 해왔습니다. 최근에는 빅데이터를 이용한 추천 플랫폼을 개발하고 있습니다. 머신러닝 및 데이터 분석에 관심이 많습니다.

Facebook LinkedIn 

공유하기

  • Luke Lee

    좋은 글 감사합니다.