[Git 기초] Branch, Merge, Rebase

저번 포스팅에서는 간단하게(?) Git의 커밋과 복구 관련에 대해 이야기했습니다.

사실 저번 커밋에서는 간단하게 변경사항, 내역들을 저장한다고 했었지만 이런 표현은 로컬 VCS 중 RCS(Revision Control System)의 Path Set이란 변경내역 저장파일에 더 어울리며 Git은 첫 포스팅에 잠시 언급하였던 스냅샷으로 파일의 버전이 저장이 됩니다.

스냅샷은 간단하게 말씀드리면 특정 시점의 메타데이터들을 저장한 것이라고 생각하시면 됩니다.

그리고 조금만 더 자세히 들어가면 Git은 커밋 시에 이런 스냅샷과 이전 커밋에 대한 포인터 등을 포함한 커밋객체를 저장하게 됩니다.

그리고 이런 커밋포인터 개념을 바탕으로 Git은 자신만의 강점으로 꼽히는 Branch 기능을 보여줍니다.


1. Branch 생성, 이동하기

Branch는 VCS(Version Control System) 중 Git의 특장점으로 뽑히는 기능입니다. 간단히 말씀드리면 기존 버전관리가 한 세계의 타임라인을 관리한다면, Git의 브랜치는 말 그대로 가지를 치듯, 특정 시점에 분기하는 평행세계를 만들어 횡적으로도 관리를 할 수 있게 됩니다.

커밋포인터와 함께 생각하면 종단이 아닌 횡단으로 건너가는 포인터를 추가하는 것으로 볼 수 있습니다.

그리고 이렇게 생긴 곁가지는 다시 본래의 가지로 합칠 수 있기에, 개인작업에서 실험적인 기능개발이나 버그픽스 작업들이 좀 더 안정적으로 가능하고 당연히 이런 기능은 협업에서 더 빛을 발휘합니다.

그리고 협업에서 이런 브랜치 기능을 좀 더 적극적이고 효율적으로 사용하기 위한 유명 Git 브랜치 전략이 몇 가지 있는데 이 중 유명한 Git-Flow에 대해 추후 간단히 포스팅 하도록 하겠습니다.

그럼 하단에는 branch를 생성하고 이동하는 기초 명령어를 소개하겠습니다.



우선 Git에서 branch를 생성하는 명령어는 다음과 같습니다.
(일단 기본적으로 Git 폴더를 만들면 master브랜치가 디폴트로 생성되어 있습니다.)

git branch 브랜치명

그리고 생성한 브랜치로 이동하는 명령어는 다음과 같습니다.

git checkout 브랜치명

참고로 아래와 같이 -b옵션을 통해 위의 생성과 이동을 한번에 처리할 수 있습니다.

git checkout -b 브랜치명



Git에서 현재 작업 중인 브랜치를 가리키는 포인터를 HEAD라고 합니다. 브래치도 일종의 포인터이므로 포인터에 대한 포인터라고 생각할 수 있습니다.

git log 명령에 --decorate 옵션을 사용하면 HEAD가 가리키는 현재 브랜치를 확인할 수 있습니다.

git log --decorate

추가적으로 아래 명령어를 통해 터미널로도 어느정도 시각화된 그래프를 볼 수 있습니다.

git log --graph --all --decorate


2. Merge 하기

Git에서 merge 명령은 말그대로 갈라진 분기의 branch의 커밋내역을 특정 시점에 다시 합치는 것입니다. 조금 다르게 이야기하면 기존branch와 갈라진branch 포인터가 같은 커밋을 가리키게 하는 것입니다.

Git에서 merge명령을 수행시키는 방법은 우선 병합을 진행할 ‘상위’브랜치로 이동 후

git checkout 병합할상위브랜치명

이후 병합시킬 하위브랜치를 merge하면 됩니다.

git merge 병합되는하위브랜치명

만약 merge후 분기되었던 브랜치가 필요없어진다면 다음 명령어로 삭제할 수 있습니다.

git branch -d 삭제할브랜치명


Git의 merge에는 fast-foward 방식과 3-way Merge 방식이 있습니다. 저희가 특별히 방식을 지정하는게 아니라 어떤 시점(갈라진 분기에서 커밋이 어떻게 진행되었는가)에 병합이 이뤄졌는가에 따라 병합되는 방식이 정해집니다.

이번 케이스는 그림이 중요한 듯하여 함께 첨부해 보았습니다. 그림의 출처는 Git 공식문서-book입니다.



fast-foward 방식

fast-foward1

fast-foward 방식은 분기한 시점에서 한쪽의 branch포인터가 이동하지 않은 (해당 브랜치에서 추가 커밋이 발생하지 않은) 상태에서 발생합니다. 위에서 master브랜치에서는 hotfix 브랜치 분기 이후 C2에서 다른 포인터 이동이 없기에 hotfixmerge로 같은 C4커밋을 가리키게 되면 아래와 같이 한 길로 이어지는 커밋 히스토리를 갖게 됩니다.

fast-foward2

iss53 브랜치의 커밋을 제외하고 보면 병합결과가 깔끔한 한줄로 이어진 깔끔한 커밋 히스토리를 갖게되는 이런 형식이 fast-foward입니다. 이 경우 히스토리도 깔끔하고 충돌(conflict)도 발생하지 않는 이상적인 케이스지만 이렇게 쉽게 되지 않는 경우가 많습니다.

그래도 Git-Flow 포스팅에서 다시 이야기할 hotfix브랜치는 배포 후 master브랜치의 예상치 않은 버그 등의 이슈를 빠르게 해결하여 적용시키기 위한 것이므로 fast-foward방식이 적용될 수 있습니다.



3-way Merge 방식

3way1

3-way Merge방식은 좀 더 일반적으로 특히 여러 작업자의 브랜치가 병합되는 협업에서 발생하기 쉬운 형태입니다. 위에서부터 이어지는 그림처럼 배포 후 특정 이슈 해결을 위한 브랜치를 분기해 작업하는 도중 급한 버그 등이 생겨 hotfix브랜치 작업이 끼일 경우 등의 상황도 생각해볼만 합니다.

이 경우에는 비유하자면 분기한 세계와 독립된 새로운 사건이 본래 세계에서 발생한 것이므로 그냥 합쳐질 수는 없고 이 둘 사이를 매끄럽게 이어갈 교차점이 될 사건이 필요로 합니다. 즉 병합시 서로의 branch 포인터가 가리키는 커밋이 서로 이어지지 않으므로 이 둘을 아우를 수 있는(양쪽으로 이동할 수 있는) 새 커밋을 생성해 두 포인터가 같은 곳을 가리킬 수 있도록 해야합니다.

(아래 그림의 경우는 병합 후 iss53브랜치를 바로 삭제해 master브랜치만 C6을 가리키고 있습니다.) 3way2

3-way Merge 방식의 문제는 이처럼 두 커밋을 아우르는 새로운 커밋을 생성하는 과정에서 충돌이 발생하기 쉽고 나중에 작업이 광대해지면 커밋 히스토리가 매우 복잡해질 수가 있습니다.

따라서 전자는 어쩔 수 없더라도 후자의 문제, 히스토리 정리를 해결하기 위해 하위 브랜치에서는 뒤에 설명드릴 rebase작업을 중간중간에 실행해 주는 것이 좋습니다.


3. Rebase 하기

merge가 두 브랜치를 합치는 것이었다면 rebase도 단어 뜻 그대로 하위브랜치의 기반이 되는 분기점을 다시 설정하는 명령입니다.

rebase

좀 더 정확히 이야기하면 분기된 experiment브랜치의 포인터가 가리키는 커밋이 새 기준점C3을 가리킬 수 있도록 기존 C4커밋에서 기반한 C4'커밋을 생성하는 것입니다. (이후 로컬에서 C4는 사라집니다.)

물론 이 경우에도 C4' 커밋을 생성하며 기존 C4C3의 커밋내용 사이에 충돌이 발생할 수 있습니다. 하지만 이제 rebase로 생성된 C4'지점에서 두 브랜치의 merge를 실행할 경우 ‘fast-forward`방식으로 처리한 것처럼 일련의 깔끔한 커밋 히스토리를 갖게 됩니다.

즉, rebase의 주 목적은 커밋 히스토리를 정리하는데 있습니다.



그리고 merge가 상위브랜치로 하위브랜치에서 실시된 실험내용들이 승인되어 합쳐지는 것이었다면, rebase는 하위브랜치의 분기가 되는 커밋을 수정하는 것이기 때문에 다음과 같이

git checkout 하위브랜치

로 기준이 되는 하위브랜치로 이동한 뒤 (merge는 기준 브랜치가 합칠 상위브랜치입니다.)

git rebase 상위브랜치

명령을 통해 하위브랜치 포인터가 가리키는 / 새로운 커밋의 포인터가 / 상위브랜치 포인터가 가리키는 / 최신 커밋을 향하게 합니다. (뭔가 말이 복잡하게 길어지네요…)



추가적으로 rebase에서 주의할 점이 있습니다. 바로 뒤 포스팅에서 나올 원격저장소에서 분기점을 가리키는 커밋(위에서는 C4)가 이미 push로 올려졌다면 rebase를 하면 위험하다는 점입니다.

왜냐하면 rebase가 단순히 커밋포인터를 옮기는 것이 아닌 위에서처럼 C4'로 새로운 커밋을 생성하고 C4를 없애는 방식으로 이뤄지기 때문에 만약 협업에서 C4커밋에 이어지는 커밋들이 생겼다면 이제 히스토리가 복잡하게 꼬이게 되어 큰 혼란이 생기기 때문입니다.

따라서 브랜치를 생성한 경우 push하기 전 rebase로 히스토리를 정리하고 이미 push가 된 브랜치라면 조심히 처리해야합니다.
(일단 위와 같이 꼬임이 발생하면 원래 push가 막히긴 하지만 --force를 쓰면 진짜 꼬이게 됩니다.)


언제나 읽어주셔서 감사합니다.^^


개인 공부용 블로그입니다.
잘못된 부분에 언제든지 댓글이나 메일로 지적해주시면 감사하겠습니다.

Leave a comment