개요
평소 Git을 사용했고, 이슈별로 브런치를 생성하며 개발했지만 혼자이다보니 Main 브랜치로 Merge 해도 코드 충돌이 발생할 확률은 극히 적었다. 협업을 시작하니 특정 브랜치로 Merge 해야하는 상황이 많이 발생했고, 코드 충돌이 잦아졌다. 발생할 이유가 없다고 생각했던 브랜치들이 충돌하는 이유, Git 트리가 상황에 따라 다르게 구성되는 이유, 아무 내용도 없는 Merge Commit 등... 여러 궁금증들이 생겼다. 이를 해소하려면 이 상황의 근간이 되는 Merge를 이해해야 했다.
Branch를 왜 만들고, Merge를 왜 하고, 코드 충돌은 왜 발생하고, Git 트리는 왜 그렇게 구성되는지를 이해해보도록 하겠다.
더 이상 code Conflict 경고에 깃죽지 않으리라.
Merge란?
Merge란 브랜치와 브랜치를 합치는 과정을 말한다.
깃을 처음 사용하면 Pull과 헷갈릴 수 있는데, Pull은 원격 리포지토리 있는 코드를 로컬 리포지토리로 받아오는, 즉 업데이트를 하는 개념이고 Merge는 합치는 개념이다.
Merge 과정에서 코드 충돌이 발생하는 이유
합치려는 브랜치가 동일한 코드를 수정할 경우 발생한다.
말은 쉬운데 생각해보면 뭔말인가 싶다. 위 내용을 보다 쉽게 이해하기 위해 깃을 사용하지 않았다면 어땠을까를 생각해봤다.
깃을 사용하지 않고 협업하기
첫째, 팀장님이 개발자 두 명에게 각각 A와 B 기능을 구현하라고 지시했다. 협업이 시작된것이다. 이 회사는 Git이나 SVN과 같은 SCM을 도입하지 않아 팀장님이 갖고 있는 Base Code를 복사해서 자신의 로컬 환경으로 옮겼다. (참고로 Base Code는 현재 운영중인 코드이다.) 이로써 개발자들은 독립적인 개발 환경을 갖추게 되었다.
둘째, 두 개발자 모두 개발, 코드리뷰, 리팩토링 과정을 거치며 성공적으로 기능 구현을 완료했다.
셋째, A 개발자가 작업한 A 기능을 선 반영하고, 하루 뒤에 B 개발자가 개발한 B 기능을 반영하기로 했다. 팀장님은 A 기능을 반영하기 위해 자신의 Base Code와 A 개발자가 작업한 코드를 병합한 후 코드를 운영서버에 반영했다. 병합은 어렵지 않았다. 팀장님이 갖고 있던 Base Code는 A 개발자에게 Base Code를 전달한 이후로 수정한 이력이 없기 때문에 A 개발자의 코드와 그대로 가져와도 됐기 때문이다.
넷째, 다음 날 팀장님은 B 기능을 반영하기 위해 Base Code(지금은 A와 병합된 코드가 Base Code이다) 와 B 개발자가 작업한 코드를 병합하려했다. 앞서 A 개발자의 코드를 그대로 가져올 순 없었다. B 개발자가 개발한 코드에는 A에 대한 기능이 없기 때문이다. 때문에 업데이트된 Base Code와 Base Code + B 코드를 비교하며 수정된 부분을 추출하여 병합하기로 했다. 그런데 이게 웬걸, A 개발자가 기능 구현을 위해 수정했던 UserService의 코드를 B 개발자도 수정한 것이다. 앞서 말한 동일한 코드를 수정한 상황이다. 이때 코드 충돌이 발생한다.
다섯째, 팀장님은 두 개발자를 호출하여 충돌이 발생한 코드 중 어떤 코드를 사용할지를 토론했다. B 개발자가 수정한 코드가 더 깔끔하고, A 개발자가 구현한 A 기능에도 문제가 되지 않아 B 개발자의 코드로 수정했다. 곧 이어 운영반영도 완료했다.
깃을 사용했다면 어땠을까?
첫째, A, B 두 개발자는 독립적인 개발환경을 마련하기 위해 Main 브랜치를 기준으로 각각 A 브랜치, B 브랜치를 생성했을 것이다.
둘째, Commit - Push - PR 의 반복을 통해 기능 구현을 마쳤을 것이다.
셋째, Main 브랜치에서 A 브랜치를 Merge 했을 것이다. 'Main 브랜치의 Head Commit (M3) 기준으로 생성된 A 브랜치'가 생성되고 난 후 Main 브랜치의 커밋 내역이 없으므로 Fast-Forward 방식으로 병합될 것이다. Fast-Forward 방식은 바로 아래 설명하도록 하겠다.
Head Commit
Branch의 마지막 커밋
넷째, 앞서 A 브랜치와의 Merge로 인해 Main 브랜치의 Head Commit이 변경됐기에 Main 브랜치와 B 브랜치를 Merge 할 경우 Fast-Forward 병합이 불가능하다. 추가로 코드 충돌이 발생했기 때문에 3-way-merge 를 통해 Merge 하고 이에 대한 Merge Commit 이 생성될 것이다.
Merge Commit
Merge 시 발생한 모든 변경사항들과 충돌 코드에 대한 대한 처리 내역을 뭉쳐놓은 커밋
Fast Forward 병합이 뭔가요?
Fast Forward Merge
브랜치 간 병합 시 현재 브랜치의 HeadCommit 과 병합하려는 브랜치의 Base Commit이 일치할 경우, 현재 브랜치의 Head Commit이 병합하려는 브랜치의 Head Commit으로 이동되는 병합 방식이다.
현재 브랜치의 Head Commit과 병합하려는 브랜치의 Base Commit이 일치...요? 🤔
Head Commit은 마지막 커밋, Base Commit은 브랜치를 생성할 때 Base가 됐던 브랜치의 마지막 커밋이다. 즉, 현재 브랜치의 HeadCommit 과 병합하려는 브랜치의 Base Commit이 일치한다는 것은, 현재 브랜치의 마지막 지점과 병합하려는 브랜치의 생성 지점이 동일하다는 것이다.
(이해가 잘 되지 않는다면 어떤 물체의 끝 지점이 다른 물체의 시작지점과 일치한다면 이 지점을 연결해주는것만으로 병합이 되는 원리로 이해해보자.)
예를들어 아래와 같이 Main 브랜치로부터 생성된 A, B 브랜치가 있다. Main 브랜치의 Head Commit은 M-C2, A 브랜치의 Head Commit은 A-C2, B 브랜치의 Head Commit은 B-C2 이고, A 브랜치와 B 브랜치 모두 Base Commit이 M-C2 이다.
Main 브랜치에서 A 브랜치를 병합할 경우 Main 브랜치의 Head Commit인 M-C2와 A 브랜치의 Base Commit인 M-C2가 같으므로 이 지점을 연결한 후 Main 브랜치의 Head Commit A-C2 브랜치로 이동시키는 Fast Forward 방식으로 병합된다.
실 테스트를 위해 main 브랜치 생성 후 main 브랜치로부터 a-branch를 생성했다. 그 후 a-branch 에서 Create-AClass, Create-BClass 라는 두 개의 Commit 을 생성하고, main 브랜치에서 a-branch 를 Merge 했다. 그 결과, Fast Forward 방식으로 병합되어 main 브랜치의 Head commit이 a-branch 브랜치의 Head commit으로 이동된 것을 확인할 수 있다.
그럼 Head Commit과 병합하려는 브랜치의 Base Commit이 일치하지 않으면 어떤일이 발생하나요? 🤔
예를 들어 설명해보겠다. 앞서 Main 브랜치가 A 브랜치와 Fast Forward 병합한 후 Main 브랜치와 B 브랜치를 병합하려는 상황이다. 현재 Main 브랜치의 Head Commit은 A-C2, B 브랜치의 Base Commit은 B-C2로 현재 Head Commit과 병합하려는 브랜치의 Base Commit이 일치하지 않는 상황이다. 이때에는 3-way-merge 방식으로 병합하게 된다. 병합한 내용은 Merge Commit 에 생성되고 아래와 같은 형태의 커밋 히스토리가 생성된다.
실 테스트를 통해 main 브랜치에서 b-branch를 병합할 경우 아래와 같이 초록색 선의 b-branch가 main 브랜치와 병합되었다는 것을 알리는 형태로 깃 트리가 생성되며, Merge Commit도 생성된 것을 확인할 수 있다.
이 외에도 현재 브랜치의 Head Commit과 병합하려는 브랜치의 Base Commit이 일치하지 않는 케이스가 있다. 바로 Base Commit을 갖는 브랜치에서 신규 Commit이 생긴 경우이다.
Main 브랜치를 기준으로 C 브랜치를 생성하고 C 브랜치의 작업자가 두 개의 Commit 을 생성하는 사이, Main 브랜치에서 M-C3 커밋이 발생했다. 이때 Main 브랜치의 Head Commit은 M-C3, C 브랜치의 Base Commit은 Merge Commit로 현재 브랜치의 Head Commit과 병합하려는 브랜치의 Base Commit이 일치하지 않아 3-way-merge 방식으로 병합되게 된다.
실 테스트를 위해 c-branch를 만든 후 Create-CClass, Create-CInterface commit 후, Main 브랜치에서 Create-MainClass라는 커밋을 생성하였다. Main 브랜치에서 c-branch 를 Merge 하니 아래와 같은 형태의 깃 트리와 Merge Commit 이 생성됨을 확인할 수 있다.
3-Way-Merge 병합은 뭐에요?
앞서 Fast-Forward 병합이 가능하지 않는 상황에 발생하는 병합으로 3개의 Commit을 통해 병합하는 방법이다. 두 브런치를 병합한다면 각 브런치의 Head-Commit 기준으로 비교하면 되지 않을까라고 생각할 수도 있지만 병합하기 힘든 케이스가 많다. 2개의 Commit 보다 3개의 Commit으로 비교하는 것이 훨씬 효율적이다. 효율적인 이유를 한번 알아보자.
아래 글에서 매우 잘 설명해주신 것 같아 해당 글을 참고했다.
https://wonyong-jang.github.io/git/2021/02/05/Github-Merge.html
효율적인 이유를 알아보기 위해 필요한 커밋은 3개로 현재 브랜치의 커밋, 병합할 브랜치의 커밋, 두 브랜치의 공통 조상이 되는 커밋이다.
Main 브랜치에 A 브랜치를 병합하는 상황을 가정하고, 메인 브랜치의 커밋을 M-C, 공통 조상이 되는 커밋을 B-C, 병합할 브랜치의 커밋을 A-C라고 가정하겠다. 생성된 커밋 히스토리는 아래와 같다.
여기서 변경된 부분이 a, b, c, d 라고 가정한다면 아래와 같은 표로 표현할 수 있다.
그럼 Base Commit 기준으로 Main 브랜치는 c, d 부분이 수정되었고, A 브랜치는 a, c 가 수정됐음을 인지할 수 있다. 그리고 이를 병합한다면 충돌이 일어난 c 부분만 작업자가 직접 병합하고 나머지는 자동으로 병합되는 상황이다.
만약 Base Commit 없이 Main 브랜치와 A 브랜치로만 병합해야한다면 어떨까? b는 둘다 똑같으니 문제되지 않지만, a와 a'는 둘 중 어떤 코드가 베이스 코드인지, 수정된 코드인지 알 수 없고, d'와 d도 마찬가지 상황에 처한다. 병합 시 a와 a', d'와 d 중 어느 코드를 적용해야할 지 모르는 상황이다. 이를 알기위해선 개발자가 직접 두 브랜치의 공통되는 커밋, 즉 조상 커밋을 찾고 어떤 부분이 수정됐는지를 체크해야한다.
결국 개발자의 편의를 위해, 그리고 효율성을 위해 3-way-merge로 병합하는 것이다.
'Git' 카테고리의 다른 글
[Git] Staging 상태 / Tracked / Untracked / Unmodified / Modified (0) | 2024.05.09 |
---|---|
[Git] Git Rebase 란? / 쉽게 이해하기 / 예시 (0) | 2024.04.22 |