Git 기초 #3 브랜치와 머지 — fast-forward와 3-way
“Git 기초 #2 add·commit·status — 스테이징 영역의 의미"에서 변경을 골라 담아 커밋을 쌓는 법을 다뤘습니다. 커밋이 하나의 줄로 쌓이기 시작하면 곧 다음 고민이 생깁니다. 잘 돌아가는 코드는 그대로 두고, 실험적인 변경을 따로 진행하고 싶은 상황입니다. 이 문제를 푸는 도구가 브랜치입니다.
총 6편으로 구성됩니다.
- #1 Git이란 — 스냅샷 모델과 세 영역
- #2 add·commit·status — 스테이징 영역의 의미
- #3 브랜치와 머지 — fast-forward와 3-way ← 이번 글
- #4 원격 저장소 — clone·fetch·pull·push와 origin의 정체
- #5 되돌리기 — restore·reset·revert 구분
- #6 GitHub와 첫 Pull Request
이번 글은 브랜치가 실제로 무엇인지부터 확인하고, 갈라진 두 줄기를 다시 합치는 두 가지 머지 방식, 그리고 머지에서 충돌이 났을 때 해결하는 흐름까지 직접 실행해 보며 정리하겠습니다.
브랜치의 정체 — 커밋을 가리키는 포인터 #
브랜치를 처음 접할 때 가장 흔한 오해가 있습니다. 브랜치를 만들면 프로젝트 폴더가 통째로 복사된다고 생각하기 쉽지만, Git의 브랜치는 커밋 하나를 가리키는 가벼운 포인터입니다. 실체를 직접 확인해 보겠습니다.
$ cat .git/refs/heads/main
1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0bmain이라는 브랜치의 실체는 커밋 해시 40자가 적힌 41바이트짜리 파일 하나입니다. 파일이 복사되는 일은 없습니다. 그래서 Git에서는 브랜치를 만드는 비용이 사실상 0이고, 하루에 몇 개씩 만들었다 지워도 부담이 없습니다.
여기에 포인터가 하나 더 있습니다. HEAD는 지금 내가 어느 브랜치 위에 있는지를 가리키는 포인터입니다. 커밋을 만들면 HEAD가 가리키는 브랜치가 새 커밋으로 전진합니다.
브랜치 만들기와 전환 #
브랜치 조작은 명령 몇 개면 충분합니다.
$ git branch # 브랜치 목록 확인 (* 가 현재 브랜치)
* main
$ git switch -c feature-login # 만들면서 바로 전환
Switched to a new branch 'feature-login'
$ git branch
* feature-login
maingit branch feature-login으로 만들기만 하고 git switch feature-login으로 전환할 수도 있지만, 실무에서는 만들면서 바로 전환하는 git switch -c를 압도적으로 많이 씁니다.
git checkout -b feature-login이 많이 보입니다. checkout은 브랜치 전환과 파일 복원이라는 서로 다른 두 일을 한 명령에 담고 있어 혼란이 컸고, Git 2.23부터 브랜치 전환은 switch, 파일 복원은 restore로 나뉘었습니다. 이 시리즈는 새 명령을 기준으로 합니다.커밋하면 포인터만 움직인다 #
feature-login 위에서 커밋을 두 번 만들면 어떻게 될까요? 움직이는 것은 브랜치 포인터뿐입니다.
[브랜치 생성 직후]
A --- B --- C ← main, feature-login (HEAD)
[feature-login 에서 커밋 2개 추가]
A --- B --- C ← main
\
D --- E ← feature-login (HEAD)main은 커밋 C에 그대로 서 있고, feature-login만 D, E로 전진했습니다. 이 상태에서 git switch main으로 돌아가면 작업 디렉터리의 파일들이 커밋 C 시점의 스냅샷으로 바뀝니다. 실험 중인 코드가 본 히스토리를 건드리지 않는 이유가 이 구조에 있습니다.
fast-forward 머지 — 포인터만 이동 #
실험이 성공했으니 feature-login의 작업을 main에 합치겠습니다. 머지는 합쳐질 브랜치로 이동한 뒤 실행합니다.
$ git switch main
$ git merge feature-login
Updating 1a2b3c4..9f8e7d6
Fast-forward
login.py | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)출력에 Fast-forward라고 적혀 있습니다. 위 다이어그램을 다시 보면, 브랜치가 갈라진 뒤 main에는 새 커밋이 하나도 없었습니다. 이 경우 Git은 새 커밋을 만들 필요 없이 main 포인터를 E까지 밀어 옮기기만 합니다. 이것이 fast-forward 머지입니다.
A --- B --- C --- D --- E ← main (HEAD), feature-login히스토리가 한 줄로 남고 머지 커밋이 생기지 않습니다. 포인터 이동만 허용하고 싶을 때는 git merge --ff-only feature-login처럼 옵션을 붙입니다. fast-forward가 불가능한 상황이면 머지를 중단해 주기 때문에, 의도하지 않은 머지 커밋을 막을 수 있습니다.
3-way 머지 — 커밋 3개로 합친다 #
이번에는 갈라진 사이에 main에도 새 커밋이 생긴 경우입니다.
A --- B --- C --- F ← main (HEAD)
\
D --- E ← feature-login이제 포인터만 옮겨서는 합칠 수 없습니다. Git은 세 개의 커밋, 곧 공통 조상인 C, main의 끝 F, feature-login의 끝 E를 비교해 양쪽 변경을 모두 담은 새 커밋을 만듭니다. 커밋 3개를 재료로 쓰기 때문에 3-way 머지라고 부릅니다.
$ git switch main
$ git merge feature-login
Merge made by the 'ort' strategy.
login.py | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)A --- B --- C --- F --- M ← main (HEAD)
\ /
D --- E ← feature-login새로 생긴 M이 머지 커밋입니다. 부모 커밋을 두 개(F와 E) 가진다는 점이 일반 커밋과 다릅니다. 히스토리에 갈라졌다가 합쳐진 흔적이 그대로 남습니다.
충돌 — 같은 부분을 양쪽이 고쳤을 때 #
3-way 머지에서 Git은 대부분의 변경을 자동으로 합칩니다. 서로 다른 파일을 고쳤거나 같은 파일이라도 다른 부분을 고쳤다면 문제가 없습니다. 자동으로 합칠 수 없는 경우는 하나입니다. 같은 파일의 같은 부분을 양쪽이 다르게 고쳤을 때입니다.
$ git merge feature-login
Auto-merging greeting.py
CONFLICT (content): Merge conflict in greeting.py
Automatic merge failed; fix conflicts and then commit the result.이때 Git은 머지를 멈추고 충돌한 파일에 마커를 남깁니다.
<<<<<<< HEAD
print("안녕하세요, 스쿨오브웹입니다")
=======
print("환영합니다, 스쿨오브웹입니다")
>>>>>>> feature-login읽는 법은 단순합니다. <<<<<<< HEAD부터 =======까지가 현재 브랜치(main)의 내용이고, =======부터 >>>>>>> feature-login까지가 머지해 오는 브랜치의 내용입니다. 해결은 에디터에서 마커를 포함한 이 블록 전체를 지우고 최종적으로 남길 코드를 직접 적는 것입니다. 한쪽을 고르든 둘을 섞어 새로 쓰든 상관없습니다.
$ git add greeting.py # 해결했다고 표시
$ git commit # 머지 커밋 완성 (메시지는 자동 제안됨)충돌은 오류가 아니라 사람의 판단이 필요하니 결정해 달라는 Git의 질문입니다. 마커를 정리하고 add, commit으로 답하면 머지가 완성됩니다. 도중에 머지 자체를 그만두고 싶다면 git merge --abort로 머지 이전 상태로 돌아갈 수 있습니다.
끝난 브랜치는 지운다 #
머지가 끝난 브랜치 포인터는 더 이상 할 일이 없습니다. 남겨 둬도 커밋이 사라지지는 않지만, 목록이 지저분해지므로 지우는 습관이 좋습니다.
$ git branch -d feature-login
Deleted branch feature-login (was 9f8e7d6).-d는 머지가 끝난 브랜치만 지워 주는 안전한 옵션입니다. 머지되지 않은 브랜치를 강제로 지우는 -D도 있지만, 커밋을 잃을 수 있으니 의미를 알고 쓸 때만 사용합니다.
마무리 #
이번 글의 핵심은 세 가지입니다.
- 브랜치는 커밋을 가리키는 41바이트 포인터라 만들고 지우는 비용이 사실상 없습니다.
- 머지는 두 가지입니다. 한쪽만 전진했다면 포인터만 옮기는 fast-forward, 양쪽이 전진했다면 공통 조상까지 커밋 3개로 머지 커밋을 만드는 3-way입니다.
- 충돌은 같은 부분을 양쪽이 고쳤을 때 사람의 결정을 요청하는 절차이고, 마커 정리 후 add, commit으로 끝냅니다.
지금까지는 모든 작업이 내 컴퓨터 안에서 끝났습니다. 다음 글인 “Git 기초 #4 원격 저장소 — clone·fetch·pull·push와 origin의 정체"에서는 저장소를 다른 컴퓨터와 주고받는 법, 그리고 많은 분들이 정체를 모른 채 쓰고 있는 origin이 실제로 무엇인지 확인하겠습니다.