Git
을 이용해 버전 관리를 할 때, 이미 만들어진 버전을 토대로 새로운 기능을 추가하거나 버그를 수정해야 하는 경우가 생긴다.
이전에 작업하던 공간에서 그대로 이어서 해도 상관없지만, 그렇게 되면 기존에 잘 작동하던 코드를 손상할 우려가 있고, 또한 여러 사람이 협업으로 각각의 기능을 개발하게 되면 서로의 작업 영역을 침범하는 일도 생길 수 있다...😥
이러한 문제를 해결하기 위해서 Git
에는 branch와 merge 기능이 존재한다.
Git
의 브랜치는 working directory
를 분리하는 기능이다!😮
생성된 브랜치는 사용 목적에 따라 크게 통합 브랜치(Integration Branch)와 토픽 브랜치(Topic Branch)로 나뉘게 된다.
브랜치는 현재 Git
의 HEAD
가 가리키고 있는 브랜치를 기준으로 git branch [name]
명령을 이용해서 생성한다.
통합 브랜치는 실제로 배포할 버전을 가지고 있는 브랜치다.
배포해야 하는 버전이므로 통합 브랜치에 있는 버전은 프로그램의 모든 기능이 정상적으로 동작해야 한다.
토픽 브랜치는 통합 브랜치에서 추가해야 할 기능이나 수정해야 할 버그 등 기존에 있던 버전을 수정하려 할 때 생성하는 브랜치다.
토픽 브랜치에서 작업한 내용을 merge를 통해 다시 통합 브랜치에 병합하는 식으로 작업을 진행하게 된다.
새로운 브랜치를 생성했다면, 자신이 작업할 브랜치를 선택할 수도 있어야 하지 않을까?🤔
그럴 때 필요한 게 바로 checkout 기능이다. checkout 명령은 아래와 같이 실행한다.
git checkout [branch name | commit id]
checkout 명령을 사용하면 Git
의 HEAD
를 원하는 브랜치를 가리키게 하거나, 혹은 commit id
를 통해 특정 version
을 가리키게 할 수 있다.
이 외에도 git checkout -- [fileName]
으로 현재 브랜치에서 modified
된 파일을 수정하기 이전으로 되돌리는 것도 가능하다!
하지만 checkout은 2.23.0
버전 이후로 switch와 restore 명령으로 분리되었으니 참고하길 바란다.😉
이제 브랜치를 나눠서 작업했으면, 작업한 결과를 하나로 합쳐야 하지 않겠는가?
이럴 때 필요한 게 바로 merge 기능이다. merge 명령은 아래와 같이 실행한다.
git merge [target]
merge는 현재 Git
의 HEAD
가 가리키고 있는 브랜치를 기준으로 진행하기 때문에, checkout
으로 먼저 HEAD
를 변경하고 사용해야 한다.
또한 merge는 브랜치를 병합할 때 fast-forward
방식 혹은 3-way merge
방식을 이용해 병합하는데, 상황에 따라 사용하는 방식이 다르다.
아래에서 두 방식의 차이에 대해 알아보도록 하자!😆
예시를 위해 다음 이미지처럼 master
브랜치에 work_together.txt 파일을 생성했다고 하자.
이제 master
브랜치에서 update
브랜치를 생성하고, update
브랜치에서 새로운 기능(예를 들면 함수)을 추가한다고 해보자.
git branch update
git checkout update
- update file
이제 update
브랜치에서 작업이 끝났으면 변경 사항을 local repository
에 commit
한 후에, 배포용 버전을 가지고 있는 master
브랜치로 이동해서 update
브랜치를 merge로 병합하도록 하자.
git commit -am "update new function"
git checkout master
git merge update
merge를 성공적으로 마치면 Fast-forward라는 문구가 보이고, master
브랜치와 HEAD
가 update
브랜치가 가리키던 버전으로 이동한 것을 볼 수 있다.
이처럼 fast-forward merge는 단순하게 merge의 기준이 되는 브랜치와 HEAD
를 더 나중에 생성된 버전(merge 대상)이 있는 곳으로 옮기는 merge이다.
fast-forward merge보다 3-way merge를 이해하는 것이 중요하다. 어떤 방식인지 보도록 하자!🙂
어떠한 브랜치를 기준으로 여러 개의 토픽 브랜치를 만들어 작업을 진행한 뒤, 해당 브랜치들을 병합하려 할 때 Git
은 기본적으로 3-way merge방식으로 병합을 진행하게 된다.
예제로 master
브랜치를 기준으로 한 명은 update
브랜치에서 기존 파일의 업데이트, 한 명은 newfile
브랜치에서 새로운 파일을 추가하는 작업을 한다고 해보자.
위의 fast-forward merge에서 썼던 예제를 이어서 사용해보겠다!
git branch update
git branch newfile
git checkout update
- update file
작업이 끝났으면 commit
을 진행하고, 새로운 파일의 추가보다 기존 파일을 업데이트하는 작업이 먼저 끝나서 update
브랜치를 우선 merge한다고 하자.
git commit -am "update new function2"
git checkout master
git merge update
두 브랜치의 병합이 Fast-forward merge로 정상적으로 완료되었고, git log
는 위와 같다.
이제 새로운 파일을 추가하는 작업을 진행해보자!
git checkout newfile
- add new file
위처럼 newfile.txt 파일을 추가했다면, 새로 생성된 파일을 git add newfile.txt
로 staging area
에 올려주고, commit
을 진행한 뒤에 git log
로 로그를 확인해보자.
git add newfile.txt
git commit -m "add new file"
git log --oneline --graph --all
보시다시피 c62c28c
버전(과거 master
브랜치의 버전)에서 두 갈래로 나뉜 것을 확인할 수 있다.
이처럼 3-way merge에서는 각 브랜치가 갈라지기 전의 원본 버전인 c62c28c
를 base라고 부른다.
이제 다시 master
브랜치로 HEAD
를 옮기고, newfile
브랜치를 병합해보도록 하자!
git checkout master
git merge newfile
master
브랜치의 내용을 보면 정상적으로 merge가 완료돼서 각 브랜치의 작업내용은 모두 병합되었으나, git log
를 확인해보면 fast-forward merge와는 다르게 merge에 대한 commit message
가 자동으로 작성된 것을 볼 수 있다.
그리고 merge 완료를 알려줄 때 Fast-forward가 아니라, "Merge made by the 'recursive' strategy." 라는 문구가 출력되는 것을 볼 수 있다.
이렇게 보면 3-way merge의 특징이 크게 드러나지 않는데, 아래의 conflict를 설명할 때 더 자세히 쓰도록 하겠다!
conflict는 브랜치 병합 도중에 각 브랜치에서 수정한 영역이 겹치는 경우에 일어나는 충돌이다. 같은 파일을 여러 사람이 분담해서 작업할 때 발생한다.
하지만 conflict가 발생한 부분만 적절히 수정해주면 다시 정상적으로 브랜치 병합이 가능해지므로 겁먹을 필요는 없다!😉
예를 들어 master
브랜치를 base로 workA
와 workB
브랜치 두 개를 생성하고, 각 브랜치에서 모두 하나의 파일을 수정하는 작업을 한다고 해보자.
base가 될 master
브랜치의 내용은 위와 같다.
이제 토픽 브랜치들을 생성하고, 각자 작업을 진행할 내용은 아래와 같이 정했다고 하자!
- Commit Ver은 1.0으로 통일
workA
브랜치에서 작업하는 사람은 3번 라인의 기능을 수정할 것.workB
브랜치에서 작업하는 사람은 2번 라인의 내용을 "merge"로 수정할 것.- 모든 토픽 브랜치에서 4번 라인에 새로운 기능을 추가할 것.
workA
workB
commit
까지 완료하고 git log
를 확인해 보면 이 전 예제에서 보았던 구조로 되어있다.
그럼 이제 master
브랜치로 HEAD
를 옮긴 후, workA
브랜치부터 병합해보도록 하자!
fast-forward merge를 이용해 정상적으로 병합된 것을 볼 수 있다.
그렇다면 남아있는 workB
브랜치도 마저 병합해보자!
workB
브랜치를 자동으로 병합하던 도중에 conflict.txt에서 conflict가 발생했다고 알려준다.
무슨 일이 일어났는지 conflict.txt 파일을 확인해 보도록 하자.
======는 해당 부분에서 conflict가 발생했다는 의미이다.
====== 를 기준으로 위쪽은 현재 HEAD
가 가리키고 있는 버전의 내용이고, 아래쪽은 병합하려는 브랜치에 있는 내용이라고 보면 된다.
이 상태로는 자동으로 병합이 불가능해서 conflict가 발생한 부분을 적절하게 수정해줘야 한다.
아래와 같이 수정하고 commit
을 해보자!
발생했던 conflict를 해결하고, 정상적으로 모든 브랜치가 병합된 것을 확인할 수 있다.
사실 위의 예제는 3-way merge가 아니라 마치 2-way merge로 병합한 것처럼 conflict가 발생했는데, 이는 버그가 아니라 Git
이 수정한 라인을 기준으로 앞뒤 라인에 수정이 생기면 conflict를 발생시키는 것 같다...😅
아마도 실제 코드를 작성할 때, line feed
없이 함수들을 붙여서 작업하는 경우는 없어서 위와 같은 일이 벌어지지는 않을 것으로 생각된다.
그렇다면 실제로 3-way merge와 2-way merge가 어떻게 다른지, conflict를 발생시키는 기준을 테이블을 통해 알아보도록 하자!😆
base | workA | workB |
---|---|---|
Commit Ver 1.0 | Commit Ver 1.0 | Commit Ver 1.0 |
base | base | merge |
base function | base function updated by workA | base function |
new function made by workA | new function made by workB |
위 테이블은 각 브랜치에서 수정한 내용들이다. row
가 line
이라고 생각하고 보도록 하자.
2-way merge는 base를 고려하지 않고, 두 브랜치의 내용만을 비교해서 conflict를 발생시킨다.
workA | workB | merge |
---|---|---|
Commit Ver 1.0 | Commit Ver 1.0 | Commit Ver 1.0 |
base | merge | conflict |
base function updated by workA | base function | conflict |
new function made by workA | new function made by workB | conflict |
두 브랜치에서 동일한 부분만 merge의 결과가 되고, 나머지 부분에서는 모두 conflict가 발생한다.
겨우 네 줄짜리 내용인데도 불구하고 내용 중 1/4이 conflict가 발생해서 일일이 수정해줘야 하는데, 만약 변경한 라인이 1,000개가 넘어간다고 생각하면 끔찍한 일이 아닐 수 없다.😱
그래서 Git
은 더욱더 많은 부분을 알아서 처리할 수 있도록 3-way merge 방식을 채택했다.
3-way merge는 두 브랜치를 병합하려고 할 때, base의 내용도 고려해서 병합을 진행한다.
base | workA | workB | merge |
---|---|---|---|
Commit Ver 1.0 | Commit Ver 1.0 | Commit Ver 1.0 | Commit Ver 1.0 |
base | base | merge | merge |
base function | base function updated by workA | base function | base function updated by workA |
new function made by workA | new function made by workB | conflict |
결과를 보면 workA
와 workB
브랜치에서 동시에 수정한 부분을 제외하고, 나머지는 Git
이 적절한 내용을 자동으로 선택해서 병합한 것을 알 수 있다.
3-way merge는 갈라져 나온 브랜치의 조상이 되는 base와 각각의 브랜치의 내용을 토대로, 두 브랜치에서 모두 수정된 내용만 conflict를 발생시킨다.
그 외에 어떠한 내용을 자동으로 병합할지 선택하는 기준은, 3개의 브랜치 중 하나의 브랜치에서만 수정된 내용을 선택해서 merge에 반영한다.
이처럼 3-way merge는 2-way merge에 비해 많은 부분을 알아서 병합해서 굉장히 편리한 병합 방식이다!🤗
구글링하다가 우연히 보게 되었는데 너무 쉽게 설명해주셔서 감사합니다!