해당 게시물은 [모두의 깃&깃허브] 책을 기반으로 작성되었습니다.
브랜치(branch)는 영어로 나뭇가지를 의미한다. 브랜치는 마치 줄기에서 뻗어나오는 나뭇가지와 같이 버전을 여러 흐름으로 나누어 관리하는 방법이다.
이번 게시글에서는 버전을 나누어 관리한다는 말이 정확히 무엇을 의미하는지, 브랜치를 어떤 상황에서 어떻게 사용해야 하는지 학습해 보겠다.
A와 B가 협업하여 온라인 쇼핑몰을 만들고 있다고 가정하겠다. 쇼핑몰은 어느 정도 완성된 상태로 코드는 방대하고 커밋이 꽤 쌓여 있는 상황이다. A와 B는 여기에 몇 가지 기능을 추가하고자 한다. A는 장바구니 기능을, B는 주문 목록 기능을 구현하기로 했다.
A와 B는 이미 어느 정도 만들어진 쇼핑몰 코드를 통째로 복사하여 각각 장바구니 기능, 주문 목록 기능을 만들 것이다. 이 과정에서 여러 파일을 추가하고 수정하고 삭제할 것이다.
A와 B가 작업을 모두 완료했다면 A와 B는 각자의 작업을 하나로 합칠 것이다. 이때 어쩔 수 없이 A와 B는 각자가 추가하고 수정하고 삭제한 코드를 하나하나 대조해봐야 한다. 이는 너무 번거롭고, 서로의 작업을 수작업으로 합치는 과정에서 실수할 수도 있다.
앞의 사례는 브랜치로 해결할 수 있다. 브랜치는 버전의 분기이다.
브랜치로 버전을 여러 흐름으로 나누어 관리한다는 말은, 쉽게 말해 다음 3단계로 버전을 관리하는 것을 의미한다.
아까 사례에서 브랜치를 어떻게 활용할 수 있을까?
A와 B는 어느 정도 완성된 쇼핑몰에서 코드를 통째로 복사하지 않고 각자의 브랜치를 나눈다. A는 쇼핑몰 코드에서 '장바구니'라는 이름의 브랜치를 나누었고, B는 '주문 목록'이라는 이름의 브랜치를 나누었다고 하자.
이제 A와 B는 나누어진 각자의 브랜치에서만 작업하면 된다. A는 장바구니 브랜치에서 장바구니 기능을 만들고, B는 주문목록 브랜치에서 주문 목록 기능을 만들면 된다.
각자의 작업이 끝나면 A와 B의 브랜치를 하나로 합친다. 그러면 A와 B의 작업은 자동으로 하나로 통합된다. 브랜치가 하나로 통합되면 쇼핑몰 코드는 장바구니 브랜치에서 A가 작업한 내용과 주문 목록 브랜치에서 B가 작업한 내용을 모두 포함하게 된다. 이때 A와 B는 '같은 코드를 다르게 수정한 부분'만 살펴보면 된다.
브랜치에 대해 더 알아보자.
깃이 제공하는 가장 기본적인, 최초의 브랜치를 master 브랜치라고 한다. 지금까지 만든 커밋은 모두 master 브랜치에 속한다. 가령 로컬 저장소를 만들고, 커밋 세 개를 만들었다고 해보자. 이 커밋 모두는 master 브랜치에 속한다.
master의 최신 커밋에서 foo라는 브랜치를 만들고, foo 브랜치에 커밋을 두 개 추가해 보자. 그렇다면 로컬 저장소는 다음과 같아진다.
여기서 주목할 점은 master 브랜치의 입장에서는 커밋이 세 개밖에 없다는 점이다. 오로지 foo 브랜치에서만 새로운 작업이 두 개 추가된 것이다. 반면, foo 브랜치 입장에서는 커밋이 다섯 개 있다. foo는 master 브랜치의 세 번째 커밋에서 뻗어나온 브랜치이기 때문에 master 브랜치의 커밋 세 개를 모두 포함하고 있다.
master 브랜치에서 커밋 한 개를 더 쌓아보자. 그럼 저장소는 다음과 같아진다.
이번에는 master의 최신 커밋에서 bar라는 새로운 브랜치를 만들고, bar에 두 개의 커밋을 추가해보자.
그러면 다음 그림과 같이 master 브랜치에는 총 커밋 네 개가, foo 브랜치에는 커밋 다섯 개가, bar 브랜치에는 커밋 여섯 개가 쌓이게 된다. 이렇듯 브랜치를 이용하면 버전을 여러 흐름으로 관리할 수 있다.
여기서 알아야 할 개념이 있다. 바로 HEAD와 checkout이다.
HEAD는 기본적으로 현재 작업 중인 브랜치의 최신 커밋을 가리키는 일종의 표시이다. 보통은 현재 작업 중인 브랜치의 최신 커밋을 가리키지만, 브랜치를 나누고 합치는 과정에서 HEAD의 위치를 자유자재로 바꿀 수 있다.
checkout이란 특정 브랜치에서 작업할 수 있도록 작업 환경을 바꾸는 것을 의미한다. 특정 브랜치로 체크아웃하게 되면 HEAD의 위치가 해당 브랜치의 최신 커밋을 가리키고, 작업 디렉터리는 체크아웃한 브랜치의 모습으로 바뀌게 된다.
가령 HEAD가 master 브랜치의 최신 커밋을 가리킬 경우, 다시 말해 master 브랜치로 체크아웃할 경우 작업 디렉터리는 총 네 개의 커밋이 만들어진 모습입니다.
가령, HEAD가 foo 브랜치의 최신 커밋을 가리킬 경우, 다시 말해 foo 브랜치로 체크아웃할 경우 작업 디렉터리는 총 다섯 개의 커밋이 만들어진 모습입니다.
가령, HEAD가 bar 브랜치의 가장 최신 커밋을 가리킬 경우, 다시 말해 bar 브랜치로 체크아웃할 경우 작업 디렉터리는 총 여섯 개의 커밋이 만들어진 모습입니다.
실무에서는 브랜치 이름을 마음대로 짓지는 않는다. 브랜치 이름을 막 지으면 이 브랜치가 무엇을 위해 만들어졌는지 알 수 없기 때문이다.
예를 들어 위 그림처럼 '새로운 기능을 개발하기 위한 브랜치' 이름은feature/<새 기능>
, '릴리스를 준비하기 위한 브랜치' 이름은release/<릴리스 번호>
, '급하게 수정해야 하기 위한 브랜치' 이름은hotfix/<수정 사항>
이렇게 명명할 수 있다.
이렇게 목적에 따라 브랜치 이름을 관리하면 브랜치 이름만 봐도 이 브랜치가 무엇을 위해 만들어진 브랜치인지 알 수 있을 것이다.
브랜치를 하나로 통합하는 것을 병합, 영어로 merge라고 한다. 브랜치를 나눈 뒤, 해당 브랜치에서 할 작업이 완료되었으면 이후 다른 브랜치와 합칠 때 사용한다.
GitKraken으로 실습해보자.
브랜치를 생성하고 하는 커밋에 대고 마우스 오른쪽 클릭을 한다. Create branch here
을 클릭한다. 그 뒤에 원하는 브랜치 명을 입력해준다.
현재 HEAD는 master 브랜치를 가리키고 있다. 새로 만든 브랜치에서 작업하려면 체크아웃을 해줘야 한다. GitKraken에서는 왼쪽에 태그같이 생긴 것을 단순히 더블클릭을 함으로써 체크아웃할 수 있다. 좌측의 LOCAL
부분을 봐도 branch가 생성된 것을 확인할 수 있다.
새로 생성한 브랜치로 체크아웃한 뒤, 로컬 저장소에 새로운 branch-test.txt
파일을 생성해준 뒤, 해당 브랜치에서 커밋해준다.
아래와 같이 feature/test1
브랜치에서 커밋된 것을 확인할 수 있다.
다음으로 마스터 브랜치로 병합해보자.
featrue/test1
브랜치를 마스터 브랜치로 병합하려면 master 브랜치로 체크아웃해야 한다. main 브랜치를 더블클릭해준 뒤, feature/test1
브랜치에서 마우스 오른쪽 클릭을 해보자.
Merge feature/test1 into main
을 클릭하자.
브랜치를 병합하고 나서 더 이상 브랜치에 남은 작업이 없다면 혹은 해당 브랜치에서 더이상 작업하지 않을 예정이라면 해당 브랜치를 삭제하는 것이 좋다.
삭제할 브랜치에 대고 마우스 우클릭을 한 뒤 Delete {branch 이름}
을 눌러주면 해당 브랜치는 삭제된다.
브랜치를 삭제하려면 삭제하려는 브랜치가 아닌 다른 브랜치에 체크아웃되어 있어야 한다.
브랜치를 병합하는 과정은 생각보다 순탄치 않을 수 있다. 앞에서는 브랜치가 한 번에 성공적으로 합쳐졌지만 그렇지 못한 상황, 즉 충돌이 발생하는 경우도 있기 때문이다. 충돌이란 병합하려는 두 브랜치가 서로 같은 내용을 다르게 수정한 상황을 의미한다. 충돌이 발생하면 브랜치가 한 번에 병합되지 못한다. 충돌은 여럿이 협업하여 개발할 때 빈번히 발생하므로 언제 발생하고, 어떻게 해결할 수 있는지 꼭 알아야 한다.
가령 master 브랜치에서 foo 브랜치가 뻗어나왔다고 하자. master 브랜치는 a.txt
파일의 첫 번째 줄을 B로 수정한 다음 커밋했고, foo 브랜치는 a.txt
파일의 첫 번째 줄을 C로 수정한 다음 커밋했다.
어떤 상황에서 foo 브랜치를 master 브랜치에 병합한다면 a.txt 파일에는 어떤 내용을 저장해야 할까? master 브랜치를 따라 B라고 저장해야 할까? 아니면 foo 브랜치를 따라 C라고 저장해야 할까?
답은 깃도 모른다이다. 이런 상황에서 깃은 어떤 브랜치의 내용을 반영해야 할지 판단할 수 없다. 이처럼 같은 내용을 다르게 수정한 두 브랜치를 병합하는 상황을 충돌이 발생했다고 한다.
브랜치를 충돌시켜보자.
새로운 로컬 저장소를 만들고 master 브랜치에 A가 저장된 a.txt
파일을 만든 뒤 이를 커밋하자.
새로운 브랜치를 생성하자. 브랜치 이름은 foo로 하겠다.
생성한 foo 브랜치로 체크아웃한 뒤, a.txt
파일에 적힌 A를 foo로 변경한 뒤 커밋한다.
master 브랜치로 체크아웃한 뒤, a.txt
파일에 적힌 A를 master로 변경한 뒤 커밋한다.
현재 foo 브랜치와 master 브랜치는 같은 내용을 다르게 수정한 상태이다. foo 브랜치는 a.txt
파일을 foo로 변경한 뒤 커밋했고, master 브랜치는 a.txt
파일을 master로 변경한 뒤 커밋했다. 이 상태에서 두 브랜치를 병합하면 충돌이 발생한다. master 브랜치로 foo 브랜치를 병합해보자.
master 브랜치로 체크아웃한 상태로 foo 브랜치를 master 브랜치로 병합하자.
바로 병합되지 않고 GitKraken 화면이 다음과 같이 나타날 것이다.
충돌이 발생하면 다음과 같이 커밋하지 않은 변경사항이 생기고, 스테이지에 올라가지 않은 항목에 충돌이 발생한 파일이 추가된다.
브랜치를 병합하는 과정에서 충돌이 발생했을 경우, 충돌이 발생한 파일들의 충돌을 해결한 뒤 다시 커밋해야만 브랜치가 올바르게 병합된다.
이때 충돌을 해결한다는 말은 같은 내용을 다르게 수정한 브랜치 중 어떤 브랜치 내용을 최종적으로 반영할지를 직접 선택한다는 뜻입니다.
스테이지에 올라가지 않은 항목에 a.txt
파일을 클릭해보자. 그럼 다음과 같은 화면이 나타난다.
좌측은 master 브랜치의 코드이고, 우측은 foo 브랜치의 코드이다. 아래는 충돌이 나지 않는 부분이며, 저장될 코드이다.
master 브랜치의 코드를 가져가고 싶으면 좌측의 빨간 네모박스를 클릭하고, foo 브랜치의 코드를 가져가고 싶으면 우측의 빨간 네모박스를 클릭한다.
코드 전체를 가져가려면 각 빨간 네모박스 위의 체크박스를 모두 클릭한 뒤 Save
버튼을 눌러준다.
둘 중 하나만 선택하는 것은 결과가 뻔히 보이기 때문에 코드 전체를 가져가보자.
빨간 네모박스 위의 체크박스를 모두 클릭한 뒤, Save
버튼을 눌러주고, commit and Merge
버튼을 눌러준다.
master 브랜치로 체크아웃한 뒤 a.txt
파일을 확인해보면 다음과 같이 master 브랜치의 변경사항과 foo 브랜치의 변경사항이 모두 추가된 것을 확인할 수 있다
이번에는 브랜치를 재배치해보겠다. 브랜치의 재배치는 rebase라고 한다.
다음 상황을 가정해보겠다. master 브랜치의 두 번째 커밋에서 foo 브랜치가 뻗어나왔고, 각 브랜치에 커밋이 여러 개 쌓여있다.
이 상황에서 다음과 같이 foo 브랜치를 네 번째 커밋에서 뻗어나오도록 변경한다. 이처럼 브랜치가 뻗어나온 기준점을 변경하는 것을 브랜치의 재배치, rebase라고 한다.
간단하게 실습해보기 위해 위와 동일한 상황을 만들어보겠다.
자 그럼 이제 foo 브랜치를 위에서 본 그림과 같이 재배치해보겠다. 브랜치를 재배치하려면, 재배치하려는 브랜치로 체크아웃해야 한다. foo 브랜치로 체크아웃하자.
재배치하려는 브랜치의 커밋에 마우스 오른쪽 버튼을 클릭한다. msater 브랜치의 4번 커밋으로 브랜치를 재배치할 예정이므로 master의 네 번째 커밋에서 마우스 오른쪽 버튼을 클릭 후 Rebase foo onto main
를 클릭한다.
그러면 다음과 같이 foo 브랜치가 master 브랜치의 네 번째 커밋으로 재배치된다. 기존에는 master 브랜치의 2번 커밋에서 뻗어나왔던 foo 브랜치가 이제는 master의 4번 커밋에서 뻗어나오게 기준점이 이동한 것이다.
물론 브랜치를 재배치하는 과정에서도 충돌이 발생할 수 있다. 충돌이 발생한다면 당황하지 말고 앞에서 살펴봤던 방식으로 충돌을 해결하면 된다.