Submodule도 Monorepo도 아닌 제3의 선택: AI + Multi-Remote Git

(수정: 2026년 3월 13일) · 5분 읽기
목차

6,300줄의 커스텀 merge 엔진을 삭제하고 “AI가 git merge 해주면 된다”로 대체했다. 대담하지만, 실제로 동작한다.

AI + Multi-Remote Git: 여러 원격 저장소를 하나의 워킹트리로 합치는 아이소메트릭 일러스트

여러 저장소의 코드를 하나의 프로젝트에 합쳐 쓰는 방법은 오래전부터 있었다. git submodule, git subtree, monorepo. 그런데 최근 AI 코딩 에이전트(Claude Code, GitHub Copilot, Cursor 등)가 보편화되면서, 예전에는 “이론적으로는 되지만 실용적이지 않다”고 여겨지던 패턴이 실제로 쓸 만해졌다. 여러 git remote를 하나의 워킹트리에 merge하고, 충돌 해결은 AI에게 맡기는 방식이다.

이 글에서는 이 패턴이 어떤 구조인지, 기존 방식과 무엇이 다른지, 실제로 어떤 프로젝트에서 어떻게 적용되었는지를 기록한다.

문제: 코드 조합의 전통적 딜레마

하나의 애플리케이션이 여러 독립적인 코드 단위로 구성되는 경우는 흔하다. 예를 들어, 코어 프레임워크에 다양한 채널 플러그인(Telegram, Slack, Discord 등)을 조합하는 구조를 생각해보자.

전통적으로 이런 구조를 관리하는 방법은 크게 네 가지가 있었다.

Submodule — 각 플러그인을 별도 저장소로 관리하고, 메인 프로젝트에 하위 폴더로 참조한다. 코드가 물리적으로 격리되어 충돌이 발생하지 않지만, .gitmodules 관리가 번거롭고 빌드 파이프라인이 복잡해진다.

Subtreegit subtree add로 외부 저장소를 하위 폴더에 병합한다. submodule보다 단순하고 히스토리가 유지되지만, 본질적으로 코드를 하위 폴더에 가두는 구조라서 플러그인이 코어 소스를 수정해야 하는 경우에는 역시 한계가 있다.

Monorepo — 모든 코드를 하나의 저장소에 넣는다. 빌드와 배포가 단순해지고, Nx나 Turborepo 같은 workspace tooling을 쓰면 대규모에서도 관리할 수 있다. 다만 독립적인 릴리스 주기가 어렵고, 모든 플러그인이 하나의 저장소에 묶인다.

npm/pip 패키지 — 각 플러그인을 패키지로 분리하고 npm install plugin-telegram으로 조합한다. 가장 깔끔하지만, 플러그인이 코어의 소스 코드를 직접 수정해야 하는 경우(예: 라우터에 import 추가) 패키지 분리가 어렵다.

공통적으로 걸리는 상황이 있다 — 플러그인이 코어 소스를 건드려야 하는 경우다. src/channels/index.tsimport { TelegramChannel } from './telegram'을 추가해야 하고, src/config.ts에 설정을 넣어야 한다면, npm 패키지로는 해결할 수 없다. submodule과 subtree도 코드가 격리되어 있어 코어 수정이 안 된다. monorepo는 가능하지만, 각 플러그인의 독립성은 포기해야 한다.

Multi-Remote Merge 패턴

이 딜레마에 대한 제4의 선택지가 multi-remote merge다. 구조는 단순하다.

# 코어 저장소
git remote add upstream https://github.com/org/core.git

# 채널별 저장소를 remote로 등록
git remote add telegram https://github.com/org/core-telegram.git
git remote add discord https://github.com/org/core-discord.git
git remote add slack https://github.com/org/core-slack.git

# 각 remote를 merge
git merge telegram/main
git merge discord/main
git merge slack/main

모든 코드가 같은 워킹트리에 존재한다. .gitmodules도 없고, 패키지 설치도 없다. npm run build 한 번이면 전체가 빌드된다. 각 채널 저장소는 코어 코드를 포함하고 있어서, 자신이 수정해야 할 파일(src/channels/index.ts 등)을 직접 변경한 상태로 관리된다.

전제 조건: git ancestry

이 패턴이 작동하려면 한 가지 조건이 있다. git merge의 3-way merge 알고리즘은 두 브랜치의 **공통 조상(merge-base)**을 찾아야 한다. 채널 저장소가 코어 저장소를 fork한 것이라면 공통 조상이 존재하고, 이후 업데이트도 incremental merge가 가능하다.

core/main ─────────────── commit A ── commit B ── commit C

telegram/main ── fork ────────┘── telegram 코드 추가

내 프로젝트 ────── merge ───────────────┘── 통합 완료

공통 조상이 없으면 — 예를 들어, 파일을 단순 복사해서 별도 저장소를 만들었다면 — git은 양쪽의 모든 변경을 충돌로 판단한다. 이 경우 최초 1회 --allow-unrelated-histories로 ancestry를 수립해야 한다.

기존 방식과 비교

항목SubmoduleSubtreeMonoreponpm 패키지Multi-Remote Merge
코어 소스 수정불가불가가능불가가능
빌드각각 별도한 번한 번한 번한 번
독립 릴리스가능가능어려움가능가능
히스토리분리유지통합분리통합
충돌 해결없음 (격리)수동수동없음AI가 해결
관리 도구git submodulegit subtreeNx/Turborepo 등npm/yarngit + AI

왜 지금 가능해졌는가

이 패턴은 이론적으로 예전부터 존재했다. Linux 커널이 수십 개의 subsystem maintainer 트리를 merge하는 것도 비슷한 구조다. 하지만 일반적인 소프트웨어 프로젝트에서는 실용적이지 않았다. 이유는 하나다.

충돌 해결이 너무 어렵다.

서로 다른 저장소가 같은 파일을 수정하면 충돌이 발생한다. 코어가 업데이트될 때마다, 채널을 추가할 때마다 충돌을 수동으로 해결해야 한다. 충돌 내용도 단순한 줄 겹침이 아니라 “이 import를 유지하면서 저 import도 추가해야 하는” 의미론적 판단이 필요하다. 사람이 매번 처리하기엔 비용이 너무 높았다.

AI 코딩 에이전트가 이 병목을 해소했다. Claude Code나 GitHub Copilot 같은 도구는 충돌의 양쪽 변경 의도를 파악하고, 두 변경을 모두 살리는 방향으로 resolve할 수 있다. 실제로 한 오픈소스 프로젝트에서는 upstream 업데이트 시 발생한 8개 충돌을 AI 에이전트가 모두 해결하고, 263개 테스트를 통과시킨 사례가 있다.

upstream 13커밋 merge → 충돌 8개 발생

               AI 에이전트가 각 충돌의 양쪽 의도 파악

               모든 fork custom 유지 + upstream 변경 수용

               263개 테스트 통과 확인

이전에는 이 과정에 별도 도구가 필요했다. 실제로 한 프로젝트에서는 3-way merge를 자동화하기 위해 6,300줄짜리 커스텀 skills-engine을 개발했다. manifest 파일로 변경 사항을 정의하고, 자체 merge 알고리즘으로 적용하는 방식이었다. 이 도구의 유지보수 자체가 프로젝트의 상당한 부담이었다.

AI 에이전트가 도입되면서 이 6,300줄이 통째로 삭제되었다. 대체한 것은 마크다운 파일 하나(SKILL.md)다. “이 remote를 fetch하고 merge해. 충돌이 나면 양쪽 변경을 모두 살려”라는 지시서.

세대방식코드량AI 의존
1세대커스텀 merge 엔진 + manifest6,300줄불필요
2세대 (현재)마크다운 지시서 + git merge~0줄필수

실제 동작하는 구조

이 패턴을 실제로 적용한 프로젝트의 저장소 구조는 다음과 같다.

flowchart TD
    core["코어 저장소<br/><small>프레임워크만, 채널 코드 없음</small>"] --> tg["코어 + Telegram"]
    core --> dc["코어 + Discord"]
    core --> sl["코어 + Slack"]
    core --> wa["코어 + WhatsApp"]

    tg --> my["내 프로젝트<br/><small>git merge로 조합</small>"]
    dc --> my
    sl -.->|"필요할 때<br/>merge"| my

    style my fill:#e8f5e9

각 채널 저장소는 코어 전체를 포함한다. Telegram 저장소는 코어 코드 + Telegram 채널 코드가 합쳐진 상태다. 내 프로젝트는 필요한 채널만 골라서 merge한다.

운영 워크플로우

일상적인 운영은 두 가지 시나리오로 나뉜다.

코어 업데이트 적용:

git fetch upstream
git merge upstream/main
# 충돌 발생 → AI 에이전트가 해결
# 테스트 실행 → 통과 확인

새 채널 추가:

git remote add slack https://github.com/org/core-slack.git
git fetch slack
git merge slack/main
# ancestry 수립 완료 → 이후 incremental update 가능

중요한 것은 최초 merge에서 git ancestry가 수립되면, 이후 업데이트는 변경분만 반영되는 incremental merge라는 점이다. 매번 전체 코드를 비교하는 것이 아니라, 지난 merge 이후의 변경만 처리한다.

한계와 트레이드오프

이 패턴이 만능은 아니다. 명확한 한계가 있다.

각 채널 저장소가 코어 전체를 중복 보유한다. 이것이 가장 큰 구조적 비효율이다. 코어에 커밋 1개가 추가되면, 모든 채널 저장소에 merge-forward CI가 실행된다. 채널이 10개면 CI 10회, 100개면 100회다. 채널 저장소가 코어를 들고 있는 이유는 3-way merge에 공통 히스토리가 필요하기 때문이다.

AI 의존이 필수다. 충돌 해결 능력이 없으면 이 패턴은 쓸 수 없다. AI 서비스 장애, API 비용, 모델 성능에 프로젝트 운영이 의존하게 된다. 전통적인 도구 체인(git + CI)만으로는 자동화가 불가능하다.

스케일 한계가 있다. 채널 수가 늘어날수록 merge 복잡도와 CI 비용이 선형 증가한다. 프로젝트 초기에는 “AI가 merge 해주면 된다”가 충분히 통하지만, 대규모에서는 근본적으로 다른 접근이 필요하다.

근본 해결: 런타임 플러그인 아키텍처

multi-remote merge는 플러그인이 코어 소스를 직접 수정해야 하는 구조에서 발생하는 우회책이다. 만약 코어가 registerChannel() 같은 확장 포인트를 제공하고, 각 채널이 npm 패키지로 분리된다면 이 패턴 자체가 불필요해진다.

# 이상적인 최종 형태
npm install @project/channel-telegram
npm install @project/channel-discord
# 코어가 자동으로 패키지를 감지하고 로드

그러면 저장소 분리도, CI 전파도, AI 충돌 해결도 모두 사라진다. 하지만 런타임 플러그인 아키텍처를 설계하는 것 자체가 상당한 작업이고, 프로젝트 초기에는 과도한 엔지니어링이다. multi-remote merge는 그 사이를 메꾸는 실용적인 중간 단계로서 가치가 있다.

정리

multi-remote merge는 새로운 기술이 아니다. git remote와 git merge는 수십 년 전부터 있었다. 새로운 것은 AI 코딩 에이전트가 충돌 해결 비용을 사실상 0으로 낮춘 것이다. 그 결과, 예전에는 리스크가 너무 높아 실무에서 쓰기 어려웠던 패턴이 실용적인 선택지가 되었다.

  • submodule / subtree — 코드를 격리한다. 충돌이 없는 대신, 코어 소스를 수정할 수 없다
  • monorepo — 모든 코드를 한 곳에 둔다. Nx나 Turborepo 같은 tooling으로 대규모에서도 관리할 수 있지만, 독립성을 잃는다
  • multi-remote merge — 독립 저장소를 유지하면서 코어 수정도 가능하다. 대신 AI가 필요하다

AI 도구가 계속 발전하면서, “사람이 직접 하기엔 비용이 너무 높았던” 워크플로우가 하나둘 실용화되고 있다. multi-remote merge는 그 초기 사례 중 하나다. 궁극적으로는 런타임 플러그인 아키텍처가 더 나은 해법이겠지만, 거기에 도달하기 전 단계에서 AI가 만들어주는 이 유연함은 꽤 유용하다.

이어서 읽기