git이 왜 복잡하게 느껴지는지에 대한 역사적 배경, UI/용어 논쟁, Mercurial·Jujutsu와의 비교, 그리고 커널 개발이라는 원래 목표를 둘러싼 토론.
URL: https://news.ycombinator.com/item?id=40486412
Title: Is there a historical reason why git is needlessly complex? Seeing this cheat sh...
네. git은 리누스 토르발스가 리눅스 커널 개발을 가능하게 하고 발전시키기 위해 특별히 만들었습니다. 이것이 이렇게 널리 채택된 건 “기쁜 우연”(혹은 불행)이죠. GitHub가(“무료 git 호스팅”) git을 이렇게 흔하게 만든 공로를 크게 인정받는 것 같지만, 많은 프로젝트에 git이 “이상적인” 버전 관리 시스템이라고는 생각하지 않습니다. 리누스가 리누스/리눅스를 위해 만들었기 때문에 용어와 때로는 워크플로가 다른 “일반적인” 버전 관리 시스템과 비교해 거꾸로인 경우가 있습니다.
“Pull Request(PR)”라는 이름도 리눅스 개발에서는 리누스/유지관리자들이 변경 사항을 리눅스 커널 트리로 PULL(끌어오기) 했기 때문에 그렇게 불립니다. 거의 모든 다른 버전 관리 시스템에서는 이게 Merge Request라고 부르는데, 변경(어디서 왔든)을 트리에 병합하는 것이니 논리적으로 훨씬 말이 됩니다.
git은(지금도?) 거대한 코드베이스를 빠른 속도로 처리할 수 있는 유일한 오픈소스 분산 버전 관리 시스템이었고, 그래서 대기업에서도 관성(inertia)을 얻었습니다. 저는 개인 프로젝트에는 Mercurial을 훨씬 선호해서 사용하지만, 큰 코드베이스(안드로이드 AOSP에서 해봤습니다)에서 Hg를 써보면 꽤 버벅입니다. Mercurial은 “사용자 인터페이스”와 커맨드 셋을 잘 잡았고(변경 사항의 트리 뷰를 보기 위해 즉석에서 웹 서버를 띄울 수 있다든지) 멋진 기능도 있지만, 파이썬으로 작성되어 성능과 확장성이 이상적이진 않습니다.
Mercurial은 “사용자 인터페이스”와 커맨드 셋을 잘 잡았고(변경 사항의 트리 뷰를 보기 위해 즉석에서 웹 서버를 띄울 수 있다든지) 멋진 기능도 있지만, 파이썬으로 작성되어 성능과 확장성이 이상적이진 않습니다.
저도 예전엔 Mercurial이 UI가 더 낫다고 생각했는데, 시간을 들여 Git을 이해하고 나서는 생각이 바뀌었습니다.
Mercurial에 멋진 기능이 있긴 하지만 Git의 작업 방식이 어렵거나 특히 직관에 반하는 건 아닙니다. 제대로 이해하려면 용어를 배워야 하지만, 그 다음부터는 수월합니다.
사실 Git은 Mercurial보다 움직이는 부품이 더 적습니다. 불필요한 기능이 더 적기 때문이죠. Mercurial처럼 “브랜치 같은” 그래프 구조 카테고리가 3개 이상 있는 대신, Git에서는 브랜치가 ref로 존재합니다. 커밋 그래프에서 이름이 붙은 모든 리프는 브랜치이거나 태그이며, 이들은 단순한 속성을 가집니다.
물론 더 많은 메타데이터를 추가하는 git 플러그인이 있겠지만, 사실 그게 꼭 필요하진 않습니다.
Git도 기본 웹 인터페이스가 있습니다: https://git-scm.com/docs/gitweb
물론 Git으로 저장소 호스팅과 전체 프로젝트 워크플로를 지원하는 GitHub 같은 솔루션도 많습니다.
저한테는 이 약속이 한 번도 맞은 적이 없습니다. 용어도 알고 “어떻게” 하는지도 아는데, clone/add/commit/push 같은 걸 넘어 일상적인 일을 어떻게 표현해야 하는지 머릿속에서 결코 명확해지지 않았습니다. 그냥 나쁘고 기억하기 어려운 UI라서, 쓰기도 번거롭고 추론하기도 번거롭습니다.
저도 뭔가를 잘 못 외우는 편이긴 한데, 다른 도구에서는 이런 수준은 아닙니다. 예를 들어 ffmpeg나 magick은 매뉴얼 없이도 어떻게든 되게 만들 수 있었어요. Git은 돌이킬 수 없게 망가뜨릴 수밖에 없습니다.
lol ImageMagick과 Ffmpeg 사용법은 기억하면서 Git은 못 한다면 전 이해가 안 가네요.
그 프로그램들은 Git과 인자 전달 방식이 꽤 다르긴 하지만, Git은 대부분의 유닉스/리눅스 프로그램과 같은 스타일을 사용합니다. 사용법을 알고 제대로 쓴다면 어떤 작업도 잃을 수 없어야 합니다.
지금 겪는 고비를 넘기려면 제 조언은 이렇습니다: 더 자주 커밋하고, 더 자주 상태를 확인하세요(git diff [--staged], git status). 그리고 커밋을 잃지 않도록 브랜치나 태그도 충분히 만드세요. 그런 다음 git rebase -i로 커밋을 편집하는 방법을 익히세요.
rebase와 cherry-pick을 쓰면(충돌은 또 다른 핵심 학습 주제이긴 하지만) 원치 않는 커밋이나 브랜치를 쉽게 정리할 수 있습니다.
뭔가 망친 것 같다면 거의 항상 마지막으로 있던 커밋으로 git reset --hard 할 수 있습니다. 어느 커밋에 있었는지 잊었다면 reflog가 있습니다.
일반적으로 Git을 잘 하려면 몇 시간 정도는 배워야 합니다. Git이 다른 도구들과 다른 점은, Git은 쓰는 과정에서 다양한 시점에 다양한 문제가 생길 수 있다는 점입니다. 반면 다른 도구들은 보통 성공하거나 실패하거나 둘 중 하나고, 대개 복구 불가능한 방식으로 실패하지는 않죠(그냥 원하는 출력이 나올 때까지 다시 실행하면 됩니다).
Git에서는 커밋 히스토리를 반복적으로 수정할 수 있는데, 그건 더 까다롭고 위험합니다.
rebase와 reflog를 포함해 보통 사용자가 알아야 할 모든 것을 설명하는 훌륭한 Git 튜토리얼이 온라인에 많습니다. GitHub에도 하나 있는 걸로 압니다. 연습과 함께 그런 걸 보면 도움이 될지도요.
ffmpeg와 magick이 git과 다른 공통점은 (상대적으로) 직관적이고 단순한 인자들이라는 점입니다. 인자는 많지만, 그 외에는 단순하고 일관적이죠. 어떤 아이디어만 기억하면 항상 같은 방식으로 동작하고 같은 모양을 합니다.
git에서는 논리적으로 관련된 연산 집합 안에서 항상 “foo”, “bar --frob” 또는 “baz :QUUX -j” 같은 게 튀어나오고, 키워드들 중 어느 것도 의미가 통하지 않습니다.
게다가 git은 세대마다 UI 방식이 달랐기 때문에, 예컨대 어떤 reset/checkout 주문을 써야 하는지 아니면 그냥 restore가 필요했던 건지 배우기가 매우 어렵습니다. 구글링하면 2015년이 아닌데도 그걸 모르는 답이 계속 나오니까요.
어떤 사람은 일관성 없고 이름도 안 좋은 것들을 학습할 수 없는데, 제가 그중 하나입니다. 기억은 할 수 있지만, 실제로 이해하려면 리누스의 마음속 기계장치를 이해해야 하는데, 그건 저에겐 외계적입니다.
어떤 흔한 Git 명령이 그렇게 이해 불가능하거나 버전마다 불안정하다고 느끼는지 모르겠네요.
구조적으로 보면 위치 인자와 비위치 인자가 섞여 있지만 리눅스에서 흔치 않은 것도 아닙니다. 기본적인 Git UI가 지난 10년 동안 크게 바뀐 적은, 제가 기억하기로는 없습니다.
물론 문제 해결을 구글링하다 보면 정석에서 벗어날 수는 있겠지만, 적절해 보이는 감이 있으면 터무니없는 접근에 속지는 않을 거라 생각합니다.
대부분 필요한 건 사실 이 치트 시트에 다 있으니, 어쩌면 참조용 자료만 필요했던 걸지도요.
you won't be taken in by hairbrained approaches.
Hare-brained가 맞습니다. (토끼(hares)는 똑똑한 동물이 아니라고 여겨지거든요.)
제목의 치트 시트를 보자마자 git add .가 “추적되지 않는 모든 파일과 스테이징되지 않은 변경”을 추가한다고 설명되어 있는 걸 봅니다. 하지만 삭제된 파일은 -A / --all을 주지 않으면 포함되지 않죠.
근데 잠깐만요, 이건 제 기억이 그렇다는 거고, 오늘 테스트해보니 이제 git은 제거된 파일도 포함하더군요. 그리고 https://git-scm.com/docs/git-add#Documentation/git-add.txt--... 도 그렇다고 말합니다.
이게 지난 10년 사이에 바뀐 건지 아닌지 판단하기가 힘든 게, 매뉴얼에는 버전도 언급이 없고 -A가 이제 기본이라는 것도 -A에 해당하는 섹션에서 말해주지 않아요.
저는 아직도 bash alias에 -A를 넣어놨고, 그건 2017년에 결국 짜증이 나서 만들었던 겁니다.
그리고 이 복잡성은 제가 처음 확인한 사소한 것에서 바로 나왔습니다. 여기서 누굴 가스라이팅하려고 몇 시간을 찾은 것도 아니고요.
Git은 코어 프로그램으로서는 빠르고 좋지만, UI, 문서, 호환성, 정보 생태계는 그냥 별로입니다.
“삭제된 파일”이란 게 무슨 뜻인가요? git rm으로 스테이징 영역에서 제거된 파일 말인가요, 아니면 파일시스템에서 실제로 지워진 경우 말인가요? (후자는 예전부터 추가하지 않았던 것 같은데요?)
저는 이런 기능들이 미완성이라고는 말하지 않겠습니다. 그냥 “추가” 기능인데, 경험에 별로 더해주는 게 없습니다. Mercurial을 좋아하는 사람들은 이게 선택지를 더 준다고 합리화하죠.
Mercurial에서는 이름 붙은 브랜치를 정말로 삭제할 수 없었던 것 같은데, 확실하진 않습니다.
Git의 브랜치에 해당하는 건 bookmarks이고, bookmarks는 Mercurial을 배울 때 보통 맨 마지막에나 접하는 것들입니다.
숙련도가 낮은 Mercurial 사용자라도 실수로 브랜치를 잃지 못하게 되어 있는 건 꽤 좋은 점이고, 대부분 사람에게 그게 가장 큰 장점입니다. 반면 숙련도가 낮은 Git 사용자는 종종 뭔가를 잃습니다.
사실 Git에서 커밋된 것을 잃는 건 reflog 때문에 정말 어렵습니다. 하지만 초보자는 reflog를 들어본 적도 없고, 그걸 설명하는 것도 매우 힘들 수 있습니다.
또 다른 큰 불만은 git reset과 그 여러 변형들입니다. 사람들은 다양한 옵션이 무엇을 하는지 이해하려고 시간을 들이지 않고, 실수로 변경 사항을 지우면 도구 탓을 하죠.
git의 역사를 이야기할 때 이해에 도움이 되는 점이 하나 있는데, git은 (당시엔 여전히 폐쇄 소스였던) BitKeeper를 대체해야 한다는 긴급한 필요에서 탄생했다는 겁니다.
bk가 git만큼 이해하기 어려운 도구라고 하진 않겠지만, 그것도 분명 복잡하긴 합니다.
git의 몇몇 특이점은, BitKeeper와 비슷한 룩앤필을 의도적으로 피한 결과일 수도 있겠다는 생각이 듭니다. 그렇게 하면 McVoy 씨를 소외시키는 일이 될 수 있으니까요.
저는 모든 일의 한가운데 있던 건 아니지만, 리눅스가 오픈소스가 아닌 Bitkeeper를 쓰고 있었다는 게 늘 이상하다고 생각했습니다.
리눅스와 GPL을 둘러싸고 그렇게 많은 싸움이 있었는데, 이건 좀 위선적으로 보이기도 했습니다.
물론 Tridge가 bk 프로토콜/데이터 구조를 리버스 엔지니어링하기 시작하면서 문제가 커졌지만, 솔직히 bk에서 벗어나야 했던 필요는 어느 정도 자업자득이었다고 봤습니다.
리눅스와 GPL을 둘러싸고 그렇게 많은 싸움이 있었는데, 이건 좀 위선적으로 보이기도 했습니다.
모든 걸 한 바구니에 담아버리면 혼란이 생기기 마련입니다. 물론 과거에도 지금도 FOSS 열성론자와 리눅스 열성론자가 있고, 그 둘의 교집합도 있습니다.
하지만 리누스 토르발스 본인은 매우 실용적인 사람이고, 필요에 더 맞는다면 폐쇄 소스 소프트웨어도 쓰겠다고 분명히 말해왔습니다. 그 입장 때문에, 더 이념적으로 순수한 일부 커널 개발자들에게 꽤 욕을 먹기도 했죠. 그 싸움을 다시 꺼낼 필요는 없겠습니다.
(아, 참고로 BitKeeper는 2016년쯤부터 FOSS입니다.)
복잡성의 일부는, 예전 버전에서는 새 서브커맨드를 추가하는 비용이 상당히 컸다는 데서 옵니다. 그래서 더 많은 기능이 git checkout 같은 기존 서브커맨드에 추가되었고, 그로 인해 UX가 더 혼란스러워졌습니다.
최근 버전에서는 새 서브커맨드를 추가하는 비용이 줄었고, git switch 같은 더 구체적인 명령들을 추가하기 시작했습니다.
대부분의 경우 git 명령 몇 개만 알아도 괜찮을 겁니다(log, add, commit, pull, push, ..?). 그리고 GUI 도구도 있고요.
솔직히 대부분의 워크플로에서 git이 그렇게 복잡하진 않다고 생각합니다. 다만 필요할 때 쓸 수 있는 강력한 기능이 많이 있죠.
복잡함이 불필요한 건 아닙니다. git이 지원하려고 만들어진 워크플로 자체가 지나치게 복잡하고, 대상 사용자도 매우 기술적인 사람들이어서 복잡성이 문제가 되지 않았을 뿐입니다.
커널 개발자들은 분산된 방식으로 서로 소통해야 했고, 서로의 작업 위에서 작업해야 했습니다. 15,000명이 코드를 쓰는 프로젝트에서 어떻게 작업하나요? Git은 이를 위해 만들어졌습니다.
Git은 설계된 워크플로를 지원하는 데 필요한 만큼만 복잡합니다. 더 단순한 도구를 만드는 건 쉬울지 몰라도, 그러면 리눅스 커널 개발 같은 복잡한 워크플로를 지원할 수 없습니다.
그래서 불필요하게 복잡한 건 아니지만, 단순한 유스케이스에는 필요 이상으로 복잡한 건 맞습니다.
덜 복잡한 프런트엔드를 원하는 사람을 위해 git과 상호작용을 쉽게 해주는 도구가 많이 있습니다. 풀타임 소프트웨어 개발자가 아닌 사람에게는 git을 직접 쓰는 걸 권하진 않습니다.
다만 풀타임 개발자라면, 근본 개념을 이해하는 데 시간을 들일 가치가 있습니다. 한번 이해하면 git은 자신이 무엇을 하는지 이해 가능한 것이 되고, 그 목적에 대해 상당히 우아합니다.
git을 아는지 여부로 소프트웨어 개발자를 가려내고 싶진 않지만, 이런 근본 시스템이 어떻게 동작하는지 스스로 파악하는 것이 도파민을 주지 않는다면, 이건 당신의 취향이 아닐 수도 있습니다. 그건 전혀 문제 아닙니다. 저도 제 취향 아닌 게 많으니까요.
git을 배우는 데는 https://learngitbranching.js.org 를 추천합니다. 물론 https://xkcd.com/1597/ 도 해당하긴 하지만요.
Jujutsu를 배워보니, 이건 정말 틀린 말이라는 걸 깨달았습니다.
기저 데이터 구조와 연산은 훌륭하고, 말씀하신 것처럼 1만 명 이상 협업 같은 목적을 매우 잘 달성합니다. 그게 git을 위대하게 만드는 부분이죠.
하지만 git의 커맨드라인 인터페이스는 그 위에 얹힌 불필요하게 복잡한 추상화이며, 특정 행위를 장려하려고 하지만 그걸 아주 못합니다.
100% 동의합니다. 같은 일을 하는 방법이 5가지나 있는 건 좋은 게 아니라 나쁜 겁니다.
문제를 해결하는 잘 정의된, 문서화된, 명확한 방법 하나를 가지세요.
가게에 가는 방법은 3가지가 있습니다. 걸어갈 수도, 차로 갈 수도, 자전거로 갈 수도 있죠. 또 경로도 3가지가 있습니다. A길, B샛길, C골목.
당신은 “그냥 해”라고 적힌 큰 버튼 하나를 누르고 싶어하는 것 같은데, 복잡한 객체에 대해 복잡한 행동을 수행하려면 같은 일을 달성하는 여러 방법이 존재하게 마련입니다.
하지만 그 가게 가는 길 중 하나가 가끔은 위험한데 대개는 안전하고, 항상 다른 2개 길만큼 빠르다면요..
게으름은 질문과 관련이 없습니다. Git에 문제가 있다는 말이 아니라, 그 비유엔 동의하지 않습니다.
하지만 위험한 길이 15분 걸리고 안전한 길은 1시간 걸린다면, 위험을 충분히 알고 있는 제가 원할 때 그 옵션을 선택할 수 있어야 하지 않나요?
누가 게으르다고 말한 적은 없습니다.
https://github.com/martinvonz/jj 를 좀 파봐야겠네요. 전엔 들어본 적이 없습니다.
어쨌든, 수행할 연산이 있는 기저 데이터 구조가 있고, 그 인터페이스는 엉망이며, 기저 데이터 구조를 배우면 모든 게 말이 됩니다.
원칙적으로는 기존 git 저장소에 대한 단순한 UI가 될 수 있다는 건 알겠는데, 그러면 그게 어떻게 동작하는지 논의하는 순간 다시 복잡해지네요.
저는 찾아보지 않고도 Jujutsu로 많은 일을 하는 방법을 말할 수 있습니다. 예를 들어 공통 조상에서 갈라진 브랜치 트리를 리베이스하는 법, 3-way 이상 머지하는 법, 한 커밋의 변경을 여러 커밋으로 나누는 법, 브랜치를 복제하는 법, 기본 브랜치에 들어가지 않은 모든 커밋을 찾는 법…
하지만 git으로 이 중 단 하나라도 어떻게 하는지, 저는 찾아보지 않고는 말할 수 없습니다.
git 경험은 대략 10년쯤 되는 것 같고, Jujutsu 경험은 3개월 정도 합산됩니다.
물론 Jujutsu도 모르는 기능이 있어 찾아봐야 할 때가 있겠죠. 하지만 제가 머릿속으로 바로 할 줄 아는 Jujutsu 연산은, 제가 git에서 할 줄 아는 연산의 진부분집합이 아니라 엄밀히 상위집합이라고 생각합니다.
git/jujutsu에서 흔한(?) 유스케이스인데 간단한 답을 못 찾는 게 있습니다.
저는 저장소를 클론합니다. 제 복사본에서 변경을 좀 합니다. 그런데 원본 저장소에서 최근에 이루어진 업데이트를 모두 섞어서 제 복사본을 최신으로 유지하고 싶습니다.
단순해야 할 것 같은데 git은 신비주의적입니다.
jujutsu는 단순하고 상식적인 CLI 명령만으로 이걸 가능하게 해줄까요? 그리고 충돌은 가까운 시일 내 정리할 수 있도록 잘 저장해둘 수 있을까요?
궁금해하는 사람들이 많습니다.
jj fetch # remote에서 가장 최근 변경 가져오기
jj rebase -b @ -d branch@remote # 현재 작업 커밋의 브랜치를 새로 fetch한 커밋 위로 rebase
참고로 git으로 하는 방식도 거의 동일하지만 덜 명확할 수 있습니다.
git에서는 아마 한 커맨드로도 가능할 거예요: git pull --rebase.
제가 원하는 상태를 오해한 거라면, 그래도 Jujutsu로 원하는 상태의 저장소를 아주 쉽게 만들 수 있을 거라고 꽤 확신합니다 :)
추가: 다만 git 방식은 충돌 정리를 즉시 강요하는 경향이 있는 반면, Jujutsu에서는 충돌을 무기한 미룰 수 있습니다.
“불필요하게”는 보는 사람에 따라 다를 수 있지만, 리누스는 UX 팀과 상의하거나 “수십 년 갈 제품”을 만들겠다는 관점 없이 당장 해결해야 하는 문제를 해결하려고 만들었으니, 설계가 “지금 당장 한 사람의 최선의 아이디어” 쪽으로 기울었을 확률이 높다고 봅니다. 그렇긴 해도, 그 팀은 엔진을 정말 잘 만들어서, 소프트웨어는 해야 할 일을 매우 잘 합니다.
수천 명 규모에서만 발생하는 복잡한 연산이 어떤 것인지 몇 가지 예를 들어줄 수 있나요?
좋은 UI 디자인은 다른 기술이 필요하고 추가적인 노력이 들어가기 때문에, 대부분 그냥 무시됩니다.
Git은 매우 단순합니다. 단 네 가지 개념으로 만들어졌습니다: blobs, trees, commits, refs.
리누스는 소프트웨어 엔지니어링 업계에서 누구보다 영향력이 컸고 “SVN 나쁨”이라고 말했습니다. 아무도 그를 반박하기 위해 충분한 조사를 하려 하지 않았고, 그래서 지금 이렇게 됐죠.
서브버전에서 머지는 완전 재앙이다. Subversion 사람들도 이걸 어느 정도 인정하고 계획이 있는데, 그 계획도 형편없다. 이 사람들이 얼마나 멍청한지 믿기 힘들 정도다.
예를 들어, 내가 Subversion 설계자들이 완전 멍청이라고 생각하는 것 중 하나로 돌아가 보자. 강한 의견. 그게 나지, 맞지? 오늘 이 방에도 몇 명 있을 것 같은데. 너희는 멍청해. (관객 웃음)
아무도 브랜칭에 관심 없다. 브랜치는 머지하지 않으면 완전히 쓸모없다.
https://sandeep.ramgolam.com/blog/linus-torvalds-talks-about...