그림을 보면 1시에 고양이는 1~3장을 작성하고, 동시에 문어는 4장을 작성합니다. 2시가 되자 고양이와 문어의 문서 작성이 끝났습니다. 문어는 ‘고양이버전1’을 가져와서 본인이 작성한 4장을 추가해서 ‘문어버전1’을 만듭니다. 문어는 3시에 고양이의 최신 버전이 있다는 것을 알았습니다. 그래서 ‘고양이버전2’를 가져와서 업데이트를 하고 ‘문어버전2’로 저장합니다.
이처럼 두 명이 동시에 버전 관리를 할 때에도 서로의 작업물에 의존하지 않고 내가 원할 때 코드를 올리고, 또 내가 원할 때 협업자의 코드와 합칠 수 있습니다.
Git에서는 이런 병렬 버전 관리를 어떻게 할 수 있을까요? 코드의 변경 사항을 묶어 하나의 덩어리로 만드는 것을 커밋이라고 합니다. 이 커밋은 줄줄이 연결되어 있는데요, 새로 만든 커밋은 기존 커밋 다음에 시간순으로 쌓입니다.
한 명이 작업한다면 위의 그림처럼 한 줄로 계속 커밋을 쌓아가면 됩니다. 그런데 두 명이 협업한다면 어떨까요? 1월 5일 버전(커밋3)을 기준으로 고양이와 문어가 각각 파일을 수정해서 커밋을 만들어야 한다고 가정해 보겠습니다. 한 줄로는 커밋을 이어나가지 못합니다. 새로운 두 커밋 모두 기준 커밋(커밋3)과 연결되어야 하니까요. 그러면 자연스럽게 두 갈래로 나뉘게 될 겁니다.
이렇게 특정한 기준 시점에서 줄기를 나누어 작업할 수 있는 기능을 브랜치라고 합니다. 새로운 가지로 커밋을 만들려면 반드시 브랜치를 먼저 만들어야 합니다. 위의 예시에서 브랜치를 만들지 않고 고양이와 문어가 둘 다 커밋3을 기준으로 커밋을 만들려고 한다면 오류가 발생합니다.
커밋을 만들었을 때 옆에 달려 있던 [main], [origin/main] 꼬리표를 보셨나요? 이것이 바로 브랜치입니다. [main] 브랜치에서 커밋을 하나 만들면 [main] 브랜치 꼬리표가 커밋1에 달립니다. 여기서 커밋을 하나 만들면 [main] 브랜치 꼬리표가 최신 커밋인 커밋2에 달리죠. 아래 그림을 보면 브랜치 꼬리표가 화살표처럼 커밋을 가리키는 것을 볼 수 있습니다. 다음 그림은 두 번째 커밋을 만들었을 때 [main] 브랜치가 두 번째 커밋을 가리키는 것을 보여 줍니다.
그런데 왜 [main] 브랜치에 커밋을 ‘올린다’라고 하지 않고 ‘가리킨다’라는 묘한 표현을 쓰는 걸까요? 이것은 브랜치가 나뭇가지처럼 물리적으로 ‘길’이 존재해서 그 길에 올리는 것이 아니라 단순한 포인터(pointer)이기 때문입니다. 포인터를 한글로 직역하면 ‘가리키는 것’입니다. 컴퓨터의 마우스 커서(포인터)를 생각하면 됩니다.
우리가 순서대로 커밋1, 커밋2, 커밋3을 만들었다고 해 봅시다. 새로 커밋할 때마다 [main] 브랜치의 포인터가 최신 커밋을 가리킵니다. 커밋2를 가리키고 있던 [main] 브랜치에서 새로 커밋하면 [main] 브랜치가 커밋3을 가리키도록 움직입니다.
커밋3의 상태에서 새로운 [고양이] 브랜치를 만들어 보겠습니다. 커밋3에서 만든 브랜치니 [main] 브랜치와 동일하게 커밋3을 가리킬 것입니다. 현재 [고양이] 브랜치와 [main] 브랜치의 상태는 모두 커밋3입니다. 만약 브랜치가 물리적이고 독립적인 길이었다면 [고양이] 브랜치, [main] 브랜치마다 커밋3을 새로 올렸을 겁니다. 그러나 브랜치가 포인터라는 것은 그저 커밋을 가리키는 것만으로도 분기를 만들 수 있다는 장점이 있습니다. 분기를 만들려면 프로젝트를 통째로 복사해야 해서 SubVersion과 같은 버전 관리 툴은 무겁고 시간이 많이 걸립니다. 반면에 Git은 가볍고 빠르게 분기를 만들 수 있습니다.
[고양이] 브랜치에 커밋을 하나 더 추가하면 아래 그림과 같이 [고양이] 브랜치가 [main] 브랜치보다 커밋 하나만큼 앞서게 됩니다.
그 상태에서 [main] 브랜치로 이동해 커밋을 하나 더 추가하면, 다음 그림처럼 이제는 [고양이] 브랜치와 [main] 브랜치의 버전이 눈에 띄게 갈라집니다. 커밋3을 기준(베이스base)으로 두 가지 버전이 생긴 거죠. 그럼 내 컴퓨터에서 이 [고양이] 브랜치와 [main] 브랜치 사이를 어떻게 넘나들 수 있을까요?
HEAD라는 특수한 포인터가 그 비법입니다. 브랜치 혹은 커밋을 가리키는 포인터죠. 우리는 [HEAD]를 이용해서 브랜치 사이를 마음대로 넘나들 수 있습니다. 아래 그림에서 [HEAD] 포인터는 커밋5 [main] 브랜치를 가리키고 있습니다. 따라서 커밋5의 상태를 보여 줍니다. [HEAD] 포인터가 커밋4 [고양이] 브랜치를 가리키게 하면 커밋4의 상태를 보여 줍니다.
브랜치의 최신 커밋이 아닌 과거 커밋으로도 [HEAD]를 이동시킬 수 있습니다. 다음 그림처럼 [HEAD]를 과거의 커밋2를 가리키게 하면 커밋2 버전을 보여 줍니다. 다만, 이 경우에는 [main] 브랜치의 포인터와 [HEAD]가 떨어져 있기에 ‘분리된 HEAD(Detached HEAD)’ 상태가 됩니다.
지금까지 그림으로 Git 브랜치의 원리를 모두 살펴보았습니다. 고마운 브랜치 덕분에 우리는 동시에 커밋을 올리거나 같은 파일을 수정하더라도 문제 없이 협업을 할 수 있습니다.
위 콘텐츠는 『팀 개발을 위한 Git, GitHub 시작하기(개정판)』으로 작성되었습니다.
이전 글 : 마이크로서비스의 6대 핵심 개념
다음 글 : [Git, GitHub] 깃, 깃허브란?
최신 콘텐츠