GN⁺: Git 브랜치 : 직관과 현실
(jvns.ca)- 많은 사람들이 Git branch의 작동 방식을 직관적이지 않다고 생각함
- Git 브랜치에 대한 일반적인 직관적 모델과 실제 Git 내부에서 브랜치가 어떻게 표현되는지에 대한 차이점을 설명함
- 직관적 모델과 실제 Git의 작동 방식이 실제로 매우 밀접하게 관련되어 있음을 보여줌
- 직관적 모델의 한계와 문제를 일으킬 수 있는 이유를 논의함
직관적인 브랜치 모델
- 많은 사람들이 브랜치를 '사과나무의 가지'에 비유하여 생각함.
- Git에서 브랜치는 '부모' 개념이 없으며, 'main'에서 분기된 것으로 생각하는 것과 다름.
Git에서 브랜치는 전체 히스토리
- Git에서 브랜치는 단순히 분기된 커밋이 아니라 모든 이전 커밋의 전체 히스토리를 포함함.
- 예제 저장소를 통해
main
과mybranch
모두 4개의 커밋을 가지고 있음을 보여줌.
브랜치는 커밋 ID로 저장됨
- Git 내부에서 브랜치는 커밋 ID를 포함한 작은 텍스트 파일로 저장됨.
- 각 브랜치의 최신 커밋이 해당 파일에 기록되어 있음.
- 커밋 간의 부모-자식 관계가 없기 때문에, 브랜치 간의 관계를 Git은 알지 못함.
사람들의 직관은 보통 그렇게 틀리지 않음
- Git에 대한 사람들의 직관이 '틀렸다'고 말하는 것은 다소 어리석음.
- '틀린' 모델도 실제로 유용할 수 있음.
리베이스는 '직관적' 브랜치 개념을 사용함
- 리베이스는 '직관적' 브랜치의 커밋만을
main
에 재적용함. - 리베이스 결과는 직관적 모델과 일치함.
머지도 '직관적' 브랜치 개념을 사용함
- 머지는 커밋을 복사하지 않지만, 공유된 기반 커밋을 필요로 함.
- 머지 베이스는 직관적 모델에 기반한 브랜치가 분기된 커밋을 찾아줌.
GitHub 풀 리퀘스트도 직관적 아이디어를 사용함
- GitHub에서
mybranch
를main
에 머지하려는 풀 리퀘스트를 생성하면, 직관적 브랜치의 커밋만 보여줌.
직관은 좋지만 한계가 있음
- 직관적 브랜치 정의는 실제 Git 작업과 잘 맞지만, Git은
main
과 분기된 브랜치를 다르게 인식하지 못함.
트렁크와 분기된 브랜치
- 사람들은
main
과mybranch
를 다르게 인식하며, 이는 Git 사용 방식에 영향을 줌. - Git은 브랜치가 다른 브랜치의 '분기'인지 구분하지 않음.
Git은 리베이스를 '역방향'으로 할 수 있음
- Git은 브랜치가 다른 브랜치의 '분기'인지 알려주지 않으므로, 언제 어떤 브랜치를 리베이스해야 하는지 사용자가 알아야 함.
-
git rebase main
과 역방향 리베이스인git rebase mybranch
가 모두 가능. 머지도 마찬가지
Git 브랜치 간의 계층 구조 부재는 다소 이상함
-
main
브랜치가 특별하지 않다는 말은 Git이 브랜치 간의 관계를 인식하지 못하기 때문에 나온 것임. - 브랜치 사이에는 관계가 있지만, git은 아무것도 모름
Git 브랜치 UI도 이상함
- '분기된' 커밋만 보고 싶을 때
git log
와git diff
를 사용하는 방법이 다름.
GitHub에서 기본 브랜치는 특별함
- GitHub는 '기본 브랜치'를 가지며, 이는 특별한 역할을 함.
GN⁺의 의견
이 글에서 가장 중요한 것은 Git 브랜치에 대한 사람들의 직관적 이해와 실제 Git의 작동 방식 간의 차이를 이해하는 것임. 이 글은 초급 소프트웨어 엔지니어들이 Git 브랜치의 개념을 더 잘 이해하고 효과적으로 사용할 수 있도록 도와줄 것임. Git 브랜치의 직관적 모델이 실제 작업과 어떻게 일치하는지, 그리고 Git이 브랜치 간의 관계를 어떻게 처리하지 않는지를 알아보는 것은 흥미롭고 유익함.
Hacker News 의견
- 브랜치는 커밋을 가리키는 포인터이며, 새로운 커밋이 생성될 때마다 이 포인터가 갱신됨. 브랜치는 태그처럼 떠돌아다니는 이름이라고 볼 수 있음. 커밋 자체가 부모 커밋을 가리키기 때문에 브랜치라는 것은 관련 커밋들의 연쇄로, 명명된 진입점을 가짐. 브랜치를 삭제하면 더 이상 명명된 레이블이 없어져서, 단순히 관련 커밋들의 연쇄로만 남게 됨.
- 커밋의 계보를 '앞으로'가 아닌 '뒤로' 가리키는 포인터로 생각하면 이해하기 쉬움. 브랜치는 커밋 ID이므로 부모 링크를 거슬러 올라가면 해당 브랜치의 전체 역사를 찾을 수 있음. '브랜치 포인트'는 두 커밋 체인이 만나는 지점이며, 병합 커밋은 특별한데, 이는 두 역사가 하나로 합쳐졌음을 나타냄.
- 개인적인 프로젝트에서
git reset --hard
와git stash
를 사용하여 변경 사항과 브랜치 포인터를 조작하는 것을 친구들이 보면 화를 내곤 함. 잘못된 병합을 취소하려면git reset --hard <병합 전 마지막 커밋>
을 사용하고, 로컬 브랜치의 작은 수정 사항을 메인 브랜치에 적용하려면git stash
를 사용한 후 메인 브랜치로 체크아웃하여git stash apply
를 사용함. - Git에는 'main이 특별하다'는 개념이 없지만, Gitlab과 같은 도구들은 보호된 브랜치 기능을 제공하여 실수를 줄일 수 있음. '부모'와 '자식' 브랜치 개념이 실제로 흥미로울 수 있으며, 장기 지원 브랜치를 위해 여러 '부모' 브랜치를 지원해야 함.
- 병합, 리베이스, 풀 리퀘스트를 할 때 다른 브랜치를 명시적으로 지정해야 함. Git은 사용자가 기반으로 생각하는 브랜치를 알지 못하기 때문임. 때로는 기능 브랜치를 다른 기능 브랜치에 병합하고 싶을 수 있으므로, 어떤 브랜치를 다른 브랜치에 병합할지 명확히 지정해야 함.
- 사람들이 가지고 있는 직관이 기술적으로 부분적으로 틀릴 수 있어도, 그들이 그런 직관을 가진 데는 타당한 이유가 있음.
-
git add
와git commit
사용법을 아는 사람들을 대상으로 한 동적인 튜토리얼이 있음. 이 튜토리얼은 브랜치를 시각화하며 읽을 수 있도록 도와줌. - Git 명령어를 수행할 때 '항상' 현재 브랜치를 수정한다는 것을 기억하면 Git의 문법을 '쉽게' 이해할 수 있음. 예를 들어,
git merge my-branch
는 현재 브랜치에 my-branch를 병합하고,git rebase my-branch
는 현재 브랜치를 my-branch 위에 리베이스함. - 브랜치(헤드)가 해당 브랜치가 시작된 기저 커밋을 가리키는 '꼬리'를 가지는 것이 좋을 것 같음. 브랜치가 자주 리베이스되기 때문에 어디서 시작하는지 생각해야 할 때가 있음. Git이 기저 커밋이
main
에 속한다고 알려주면 더 편리함. - 메일링 리스트에 '패치'를 보낼 때 기저 커밋을 선택적으로 포함할 수 있음. 이는 변경 사항이 최신 릴리스, 메인 개발 브랜치, 또는 통합 브랜치 중 어디에 기반하는지 명확하지 않을 수 있기 때문임.
git range-diff
를 사용할 때도 기저를 염두에 두어야 함. 이 도구는main..previous
와main..current
와 같은 두 범위를 비교함. - 브랜치에 대한 개인적인 견해를 다시 읽고 잊었던 몇 가지를 다시 배움.