Git의 낡은 Merge UX와 CRDT 기반 버전 관리의 가능성: Manyana 톺아보기
15년 넘게 현업에서 수많은 코드를 다루면서 확신하게 된 사실이 하나 있다. 세상의 어떤 엔지니어도 충돌(Conflict)을 해결하는 과정을 즐기지 않는다는 것이다. 특히 수명이 긴 피처 브랜치를 Main 브랜치에 Rebase 할 때 화면을 가득 채우는 <<<<<<< HEAD 마커를 마주하면 한숨부터 나오기 마련이다.
최근 BitTorrent의 창시자로 잘 알려진 Bram Cohen이 Manyana라는 흥미로운 프로젝트를 공개했다. 이는 CRDT(Conflict-Free Replicated Data Types)를 기반으로 한 버전 관리 시스템(VCS)의 데모 프로젝트다. 과연 CRDT가 Git이 지배하는 이 생태계에 새로운 패러다임을 제시할 수 있을까? 아니면 그저 또 하나의 지나가는 장난감에 불과할까?
아키텍처의 차이: Snapshot DAG vs CRDT Weave
Git의 근간은 스냅샷과 DAG(Directed Acyclic Graph)다. Merge를 수행할 때 Git은 두 브랜치의 공통 조상(Common Ancestor)을 찾아 3-way merge를 수행한다. 이 과정에서 텍스트 기반의 휴리스틱이 실패하면 우리가 아는 그 끔찍한 충돌 마커를 뱉어낸다.
반면 Manyana는 CRDT를 사용한다. CRDT의 수학적 특성상 병합은 ‘항상’ 성공한다. 데이터 구조 자체가 충돌 없이 병합되도록 설계되었기 때문이다. Manyana는 파일의 상태를 단순한 텍스트의 스냅샷이 아니라, 파일에 존재했던 모든 줄(Line)의 추가 및 삭제 메타데이터를 포함하는 단일 구조, 즉 Weave (직물) 형태로 관리한다.
이로 인해 얻는 가장 큰 표면적 이점은 훨씬 직관적인 Conflict UX다.
기존 Git의 충돌 마커는 다음과 같이 두 개의 불투명한 덩어리를 보여준다.
<<<<<<< left
=======
def calculate(x):
a = x * 2
logger.debug(f"a={a}")
b = a + 1
return b
>>>>>>> right
Manyana는 이를 구조적으로 분해하여 보여준다.
<<<<<<< begin deleted left
def calculate(x):
a = x * 2
======= begin added right
logger.debug(f"a={a}")
======= begin deleted left
b = a + 1
return b
>>>>>>> end conflict
누가 함수를 지웠고, 누가 그 중간에 로그 라인을 추가했는지 명확하게 알 수 있다. Bram은 이처럼 “병합 자체는 항상 성공하되, 변경 사항이 너무 가까이 붙어있을 때만 리뷰를 위해 충돌로 표시(Flag)하는 것”이 미래의 VCS가 가져야 할 모습이라고 주장한다.
시니어 엔지니어의 시선: 굳이 새로운 VCS가 필요한가?
솔직히 말해서 나는 이 프로젝트를 처음 보았을 때 약간의 회의감이 들었다. Bram이 지적한 Git의 UX 문제, 특히 Rebase 시의 혼란스러움에는 100% 동의한다. Hacker News 스레드에서도 많은 개발자들이 공감했듯, git rebase를 할 때 ours와 theirs의 의미가 뒤바뀌는 현상은 시니어 개발자들조차 매번 구글링을 하게 만드는 Git 최악의 디자인 중 하나다.
하지만 이 UX 문제를 해결하기 위해 VCS의 기반 아키텍처를 CRDT로 완전히 갈아엎어야 할까?
HN 커뮤니티의 여러 엔지니어들이 날카롭게 지적했듯, 우리는 이미 도구 레벨에서 이 문제를 어느 정도 해결하고 있다.
- Git Native:
git config merge.conflictStyle zdiff3를 설정하면 공통 조상(Base)을 포함하여 보여주므로 Manyana와 유사한 수준의 컨텍스트를 얻을 수 있다. - External Tools: p4merge, IntelliJ IDEA, VSCode 등 3-way 또는 4-way 머지를 지원하는 훌륭한 도구들이 널널하게 존재한다.
즉, 프레젠테이션 계층(Presentation Layer)의 문제를 해결하기 위해 데이터 저장 계층(Storage Layer)을 바꾸는 것은 전형적인 오버엔지니어링으로 보일 수 있다.
Semantic Conflict: CRDT가 해결하지 못하는 진짜 문제
내가 Manyana와 같은 접근 방식에서 가장 우려하는 부분은 ‘의미론적 충돌(Semantic Conflict)‘이다.
CRDT는 텍스트가 겹치지 않는 한 병합을 수학적으로 성공시킨다. 하지만 코드가 텍스트 레벨에서 충돌 없이 병합되었다고 해서, 그 코드가 정상적으로 동작한다는 보장은 전혀 없다. 개발자 A가 함수의 이름을 변경하고, 개발자 B가 다른 파일에서 기존 이름으로 그 함수를 호출하는 코드를 추가했다고 가정해 보자. CRDT 기반 시스템(또는 Git의 자동 병합)은 이를 아무런 경고 없이 병합해 버린다.
결과는? 빌드 실패다. 만약 운이 나빠서 빌드마저 통과한다면(예: 메모리 배리어가 잘못된 위치에 삽입된 경우), 이는 프로덕션 환경에서의 끔찍한 장애로 이어진다. HN의 한 유저가 남긴 “컴파일이 안 되는 것은 차라리 다행이다. 진짜 위험한 것은 컴파일이 되는 경우다”라는 코멘트는 현업의 뼈저린 경험을 그대로 보여준다.
결국 버전 관리에서 ‘충돌’이란 피해야 할 악이 아니라, 개발자에게 “이 부분의 컨텍스트가 변경되었으니 반드시 수동으로 확인하라”고 강제하는 중요한 안전장치다. CRDT가 이 안전장치를 너무 쉽게 우회하도록 만든다면, 그것은 오히려 독이 될 수 있다.
그럼에도 불구하고: History 보존과 Rebase의 재발견
비판적인 시각을 유지하긴 했지만, Manyana의 디자인 문서에서 내 눈길을 사로잡은 매우 훌륭한 아이디어가 하나 있다. 바로 ‘히스토리를 파괴하지 않는 Rebase’ 다.
Git에서 Rebase는 본질적으로 기존 커밋을 버리고 새로운 커밋을 만들어내는 파괴적인 작업이다. 이로 인해 브랜치를 공유할 때 지옥이 펼쳐진다. 하지만 CRDT 시스템에서는 모든 히스토리가 Weave 구조 안에 남아있으므로, DAG에 ‘Primary Ancestor(주 조상)‘라는 메타데이터만 추가하면 히스토리를 온전히 유지하면서도 Rebase와 동일한 깔끔한 선형 구조를 만들어낼 수 있다.
이 접근법은 Git의 가장 큰 딜레마인 “선형적인 깔끔한 히스토리 vs 실제 작업 내역의 보존”이라는 문제를 근본적으로 해결할 수 있는 잠재력을 가지고 있다.
결론: 프로덕션 레디? 아직은 장난감, 그러나 훌륭한 자극제
Manyana는 현재 470줄 남짓의 Python 코드로 이루어진 개념 증명(PoC) 데모다. 당장 내일 회사 프로젝트의 Git을 대체할 물건은 절대 아니다. Cherry-pick이나 Local Undo 같은 필수 기능도 아직 설계 단계에 머물러 있다.
하지만 이 프로젝트는 우리가 너무나 당연하게 받아들이고 있던 Git의 한계(특히 DAG 기반 병합의 복잡성과 Rebase의 파괴성)를 다시 한번 돌아보게 만든다. Pijul이나 Jujutsu(jj) 같은 차세대 VCS들이 부상하고 있는 지금, 데이터 구조 레벨에서 버전 관리를 재정의하려는 시도는 그 자체로 엄청난 가치가 있다.
당장 Manyana를 쓸 일은 없겠지만, 오늘 출근하면 동료들에게 merge.conflictStyle zdiff3 설정은 꼭 전파해야겠다.