2P by GN⁺ 3시간전 | ★ favorite | 댓글 1개
  • Git은 소스의 분산 저장소로는 성공했지만, 분산 워크플로 처리는 나중에 붙은 해법에 가까워 한계가 드러남
  • Git의 커밋과 브랜치는 후속 커밋, amend 이력, rebase 이력, 버려진 상태를 스스로 표현하지 못함
  • Stacked PR에서는 후속 PR을 찾고 스택을 유지한 채 rebase해야 하지만, Git은 이 관계를 안정적으로 파악하기 어려움
  • Git은 staging, unstaged, 파일 시스템, HEAD 같은 변경 가능한 상태를 커밋·브랜치 바깥에 두어 학습과 사용을 복잡하게 만듦
  • 병합 전 PR들을 함께 써야 하는 비동기 개발 흐름에서 Git의 뒤를 향한 불변 이력 모델은 반복적인 문제를 낳음

Git의 두 역할

  • Git은 소스의 분산 저장소이면서 동시에 분산 워크플로 도구로 쓰임
  • 소스 저장소로서는 매우 성공했지만, 분산 워크플로를 다루는 방식은 대부분 나중에 덧댄 해법에 가까움
  • 비동기 개발은 East River Source Control의 표현처럼 기본 조건에 가까우며, 시간대가 다른 협업뿐 아니라 자기 자신과 시간차를 두고 일할 때도 발생함
  • jj는 Git의 한계를 더 선명하게 드러내는 도구이며, Git이 충분하다고 느끼는 사람은 jj를 진지하게 시도하지 않을 가능성이 큼

Git의 기본 모델이 놓치는 관계

  • Git 사고의 중심에는 커밋과 브랜치가 있음
    • 커밋은 소스 코드와 이력을 담는 불변 객체임
    • 브랜치는 로그가 붙은 변경 가능한 포인터임
  • 전형적인 Git 다이어그램은 커밋을 C1, C2, C3처럼 그려 순서와 관계가 명확해 보이게 하지만, 실제 저장소의 커밋 이름은 해시나 메시지에 가까워 그런 순서 관계가 시스템 안에 존재하지 않음
  • rebase 후의 C2C2’ 같은 표기는 사람이 이해하기 쉬운 설명일 뿐, Git은 두 커밋이 서로 대응된다는 사실을 알지 못함
  • 특정 커밋의 후속 커밋을 찾으려면 모든 브랜치를 훑어 해당 커밋으로 이어지는 경로의 커밋을 찾아야 하므로 간단하지 않음

Git에는 “C”가 없음

  • Git 커밋은 다음 정보를 스스로 알 수 없음
    • 후속 커밋

      • amend 이후 새 커밋에서 예전 커밋으로 이어지는 수정 이력
    • rebase 이력

      • 해당 커밋이 버려졌는지 여부
      • 브랜치에도 한계가 있음
      • 브랜치는 이력 개념은 있지만, 코드 변경과 1:1로 대응된다고 신뢰하기 어려움
      • 브랜치끼리는 관계를 갖지 않으며, 예컨대 trunk에서 wp/bugfix를 안정적으로 찾을 수 없음
      • trunk에서 wp/bugfix로 향하는 전방 참조가 없기 때문에 도달 가능한 관계도 아님
      • Git 다이어그램은 사람이 보기에는 순서와 대응 관계가 있어 보이지만, 실제 도구가 제공하는 능력을 과장해 보일 수 있음

Stacked PR이 어려운 이유

  • 시간대가 다른 사람과 협업하면서 리뷰 전에는 병합하지 않으려면, CPU처럼 작업을 파이프라이닝해야 함
  • 하나의 PR을 만들고 리뷰가 끝날 때까지 기다리는 대신, 첫 PR 위에 두 번째 PR을 만들고 다시 그 위에 다음 PR을 쌓아 여러 순차 PR을 동시에 리뷰에 올리는 방식이 Stacked PR
  • Git은 Stacked PR 구조를 안정적으로 다루기 어렵게 만듦
    • Fix key entry race 위에 Refactor key entry code 같은 후속 PR을 만들고, 이후 trunk를 fetch해 갱신하면 스택을 유지한 채 rebase해야 함
    • Git은 후속 커밋을 알지 못하므로 Fix key entry race에서 Refactor key entry code를 쉽게 볼 수 없음
    • 커밋이 버려진 것일 수도 있어, 후속 커밋을 볼 수 있더라도 최신 상태인지 알기 어려움
    • 브랜치는 PR 자체처럼 쓰이지만 이 흐름에서는 실수로 덮어쓰기 쉬움
  • Graphite 같은 스태킹 도구는 Git 위에서 이 작업을 할 수 있지만, Git의 커밋이나 브랜치 자체를 보강하지는 못함
    • 별도의 브랜치 메타데이터 저장소를 만들고 Git과 동기화해야 함
    • 사용자가 Git 자체를 직접 조작하면 그 저장소가 Git 상태와 어긋날 수 있음

변경 가능한 상태가 커밋 바깥에 있음

  • Git의 여러 문제는 변경 가능성(mutability) 을 직접 모델링하지 않는 방식에서 이어짐
  • Git의 편집 워크플로에는 커밋과 브랜치 바깥에 별도 상태가 존재함
    • Staging 또는 index는 작업 복사본에서 만들어지는 소스 스냅샷이며, 새 커밋은 여기서 만들어짐
    • Unstaged는 index와 파일 시스템 사이의 차이를 나타내는 두 번째 diff임
    • 파일 시스템은 checkout된 내용을 담고, 여기에 staged와 unstaged 변경이 더해짐
    • HEAD는 새 커밋이 만들어지는 위치임
  • stash는 staging과 unstaged 변경을 저장하고 복원하는 별도 저장소처럼 동작함
  • checkout을 다른 커밋이나 브랜치로 바꾸면 Git은 파일 시스템을 새 위치에 맞추면서도 staging 또는 unstaged의 diff를 보존하려고 함
  • 이 과정은 명령은 다르지만, 화살표 관계만 보면 staging을 새 기준 위로 옮기는 rebase와 비슷한 형태를 가짐

모든 것을 커밋으로 모델링하기 어려운 이유

  • Staging과 작업 복사본도 명확한 조상을 갖고 소스 코드를 담으므로, 정적인 상태만 보면 커밋처럼 표현할 수 있음
  • 하지만 커밋 ID는 내용의 해시이기 때문에 커밋이 변경 가능하다면 ID가 계속 바뀜
  • Staging과 작업 복사본이 “무엇인지”를 일관되게 가리키려면 커밋이 아니라 브랜치처럼 다뤄야 하지만, 브랜치에는 앞서 다룬 한계가 있음
  • 이 복잡성은 실제 문제로 이어짐
    • Git 학습과 사용이 더 어려워짐. 같은 개념이 양쪽에 따로 존재하기 때문임
    • 저장소 전체 상태가 clone으로 가져오는 상태와 크게 달라 내보내기가 어색해짐
    • 시간이 지나며 변경 세트가 바뀌는 비동기 흐름이 잘 동작하지 않음
    • 변경 가능한 쪽의 시스템은 merge를 표현하지 못해 실제 워크플로를 나타낼 수 없는 경우가 있음

Git이 실제 워크플로를 표현하지 못하는 경우

  • 새 기능 브랜치에서 아직 커밋하지 않은 채 개발하던 중, 기기에서 개발을 방해하는 버그를 발견할 수 있음
  • 해당 버그가 새 기능을 막지는 않지만 개발을 성가시게 만든다면, 작업을 stash하고 새 브랜치로 이동해 재현 테스트와 수정 작업을 만든 뒤 PR을 올릴 수 있음
  • 이후 다시 새 기능 브랜치로 돌아오면 선택지가 제한됨
    • new-feature를 실제 의존성이 없는 bugfix 위로 rebase하고 리뷰를 진행함
    • 개발 중에는 new-featurebugfix 위로 rebase해 쓰다가, 브랜치를 제출하기 전에 rebase를 되돌림
  • Git으로는 “편집 작업 공간에는 bugfix의 모든 코드와 이미 커밋한 new feature 코드가 함께 있어야 한다”는 상태를 표현할 수 없음
  • 이런 요구는 병합되지 않은 PR과의 호환성 테스트 같은 더 어려운 문제에서도 같은 구조로 나타남
  • Jujutsu megamerges처럼 적절한 도구를 쓰면 여러 PR을 병렬로 유지하면서도 편집 공간에서는 함께 사용할 수 있음

Git은 더 이상 충분하지 않음

  • 2000년대 초반의 버전 관리 도구들은 사용과 관리가 어렵고 품질이 들쭉날쭉했으며, Subversion도 고통스럽다는 인식이 널리 퍼져 있었음
  • 당시에는 로컬에 전체 저장소 복사본을 갖고 싶다는 요구가 일반적이지 않았고, 로컬 브랜치를 만들고 싶다는 요구도 보편적이지 않았음
  • 파일 잠금에 불편을 느끼는 사람도 많았지만, 어떤 사람들은 파일 잠금이 필요하다고 보았고 Git에서 개별 파일이나 디렉터리를 잠글 수 있는지 묻기도 했음
  • 오픈소스처럼 분산 워크플로를 직접 겪던 사람들에게 DVCS는 오래된 상처를 막아주는 붕대처럼 받아들여졌음
  • 오늘날 의미 있게 분산된 워크플로를 쓰는 사람에게 Git의 뒤를 향한 불변 이력 모델은 반복적인 문제의 원천이 됨
  • Meta 같은 회사는 거의 10년 동안 Git을 크게 앞서는 사내 시스템을 사용해 왔음
  • “이제 Git은 Claude가 대신 만진다”는 흐름이 이런 대안을 무의미하게 만들지는 않음
  • LLM을 쓰면서 단일 머신 안에서도 엔지니어들이 이전보다 더 많은 비동기 개발을 하고 있는 것으로 보임

댓글과 토론

Lobste.rs 의견들
  • 글에서 제기한 문제를 jj가 어떻게 해결하는지 보여줬으면 좋았을 듯함
    jj 사용자에게는 뻔하겠지만, 그 사람들이 글의 주 대상은 아닐 가능성이 큼

  • 글에서 Git이 괜찮지 않다는 근거로 든 기능들이 개인적으로는 필요했던 적이 없음
    나만 그런 건가 싶음

    • 전혀 혼자가 아님
      도구에서 중요한 점 중 하나는 도구가 동적인 시스템의 일부라는 것임. 도구가 가능하게 해주는 일이 “내가 할 수 있다고 믿는 일”에 영향을 주고, 그 믿음이 다시 도구에 대한 인식과 도구의 진화 방향을 바꿈
      어떤 도구가 기존 상태를 흔들면, 할 수 있는 일에 대한 믿음과 기대치도 같이 바뀜
  • 흥미로워 보이지만 다이어그램을 보면 어지러움

  • 2000년대 초반처럼 지금이 심각하진 않고, Git 이전 버전 관리 시스템의 한계는 꽤 명확했다는 말에 대해, Darcs는 Git보다 먼저 나왔고 스냅샷 기반 버전 관리의 일부 문제를 근본적으로 고친 면이 있었음
    초기에 성능이 나빠 밀렸지만 이후 성능은 개선됐고, 사람들이 다시 확인하러 돌아오지 않았음. 흥미로운 일을 하는 다른 버전 관리 시스템도 있으니 “Git이 아니면 Jujutsu”만 유일한 선택지처럼 다루지는 않았으면 함. 그런 식의 논리를 너무 자주 봄

    • 글쓴이가 Git의 데이터 모델은 매우 좋다고 말해놓고, Git의 브랜치가 단지 브랜치 끝을 가리키는 포인터라서 작업 흐름이 좋지 않다고 지나가듯 말하는 게 좀 웃김
      그것도 데이터 모델 문제임
  • jj는 이걸 어떻게 처리함? https://www.billjings.com/posts/title/git-is-not-fine/RealityEx23.png

    • jj new A B를 쓰면 작업 복사본 커밋이 여러 부모를 가질 수 있어서, 병합 커밋처럼 동작함
      그래서 작업 복사본에 두 부모의 변경이 모두 들어오고, 그 병합 위에서 작업을 이어가거나 어느 한쪽 커밋에 amend하는 식으로 진행할 수 있음
  • 아직은 Git을 더 선호하고, 글쓴이에게는 편향이 있어 보임

    • Git을 선호하는 이유가 글쓴이가 Git에서 어렵다고 말한 상황을 겪지 않고 이미 Git에 익숙해서인지, 아니면 그런 상황을 겪더라도 Git이 Jujutsu보다 더 나은 작업 흐름을 제공한다고 생각해서인지가 궁금함
    • jj new를 실행해야 한다는 점만 기억하면 gitjj를 섞어 쓸 수 있음
      Git은 항상 부모 커밋을 가리키고, 현재 jj commit은 작업 트리의 커밋되지 않은 변경처럼 보게 됨
      나는 그렇게 jj를 배웠음. 리베이스 처리나 트리 이동처럼 jj가 잘하는 일에는 jj를 쓰고, 아직 jj 대응 명령을 모르거나 git blame처럼 Git 명령이 먼저 떠오르는 일상 작업에는 계속 git 명령을 썼음
      실제로 매일 써보기 전까지는 jj가 왜 더 나은지 잘 와닿지 않았고, 읽기만 했을 때는 “이 기능이 꼭 필요할까” 또는 “Git으로도 이미 할 수 있는데”라고 생각했음
      물론 jj에도 단점은 있음. 최신 .gitignore가 없으면 바이너리 파일이 실수로 커밋에 들어갈 수 있음. 다행히 아주 큰 파일을 추가하려 하면 jj가 경고하지만, 작은 파일은 빠져나갈 수 있음
      디버깅 중 현재 디렉터리에 추적 파일이나 로그 파일이 있으면 그것도 들어갈 수 있으니, 트리를 다 조작한 뒤에는 diffstat을 모두 검토하는 게 좋음
      특히 jj로 이진 탐색을 하다가 .gitignore를 갱신한 커밋보다 이전 커밋을 테스트하게 되면 문제가 될 수 있음. 이진 탐색에는 읽기 전용 모드가 있어야 할지도 모름