Git 브랜치 전략과 Git Merge vs Rebase

 

이번 프로젝트에서 팀장 역할을 맡게 되었다. 이전 프로젝트들에서 어느 부분이 가장 중요했을까를 다시 한 번 생각해보았다. 다수의 프로젝트를 진행하면서 꼭 한 번씩은 Git으로 인한 문제가 발생했었다. Git에 익숙하지 않았던 내 첫 프로젝트에서 팀원의 코드를 내 코드로 덮어버리는 대형 사고를 쳤던 덕분에, 이후로는 Git 사용에 있어 항상 신중하게 생각하고 있다. 이에 항상 Git 브랜치 전략이나 Git 명령어를 문서화하는 역할을 담당하고 있다.

 

오늘도 어김없이 Git 사용과 관련한 메뉴얼을 작성하던 중, Git 브랜치 전략을 고민하게 되었고 Git Merge와 Rebase의 차이점에 대해 생각해보게 되었다. 매번 하는 작업이지만, 이 차이점을 말로 설명하려 하니 명확하게 설명하기 어려워서 자료를 참고하며 학습한 내용을 포스팅한다.

 

1. Git

https://syh39.github.io/git/git/

Git은 Version Control System 방식 중 하나로, 소스 코드의 형상을 관리하는 데 쓰인다. 또 다른 VCS인 SVN과 달리, Git을 사용하면 각 사용자가 전체 저장소의 히스토리 복사본을 로컬 환경에 갖고 있게 되어, Distributed(분산형) VCS로 불리기도 한다. 

 

 

2. 브랜치 전략

Git을 잘 활용하기 위해서, 브랜치를 어떻게 관리할 것인지에 대한 기준이 필요하다. 크게 Git flow와 GitHub flow 전략이 있다.

 

Git flow

 

Git flow 전략은 feature, develop, release, hotfix, master 5가지의 브랜치로 설명할 수 있다.

- feature: 기능 개발
- develop: 개발 환경
- release: 배포 환경
- hotfix: 운영 환경의 문제 수정
- master: 운영 환경

브랜치 별 책임을 명확히 하므로 각 브랜치에서의 작업이 다른 브랜치의 작업으로 인해 딜레이되는 경우가 없고,  애플리케이션의 배포 버전을 확실하게 관리할 수 있다. 하지만, 브랜치가 많아 복잡하고 규칙이 구체적이어서 적용하기 쉽지 않다.

 

GitHub flow

 

GitHub flow는 Git flow의 복잡함을 해소하고자 제안된 전략이다. GitHub flow 전략은 master, feature 2가지의 브랜치로 설명할 수 있다.

- master: 개발 환경, 운영 환경
- feature: 기능 개발

master 브랜치 하나로 개발 환경과 운영 환경을 함께 관리한다. 잘못된 코드가 통합되는 경우, 운영 환경에 심각한 장애를 일으킬 여지가 있기 때문에 기능 개발 후 코드 통합 시 테스트가 확실하게 진행되어야 한다. 수시로 배포가 일어나기 때문에 CI/CD가 구축되어 있는 프로젝트에 적용하면 유용하다.

 

 

두 가지 중, 우리 프로젝트에서는 Git flow 전략을 선택했다. GitHub flow에 비해 복잡하긴 하지만, 프로젝트에 대한 CI/CD 구축 여부가 확실하지 않은 상황에서 GitHub flow를 선택한다면 코드의 안정성이 떨어질 것이라 생각했다. 또한, 우리의 프로젝트에서 API path에 버전을 명시하고 있었기 때문에, 배포 버전을 관리할 수 있는 Git flow 전략이 적합하다고 판단했다.

 

결국 어떤 전략을 선택하더라도, Git을 활용하게 되면 각 사용자가 로컬 환경에서 개별적으로 작업을 진행할 수 있다. 이 때문에 반드시 개별 작업 내용을 통합하는 과정이 수반된다. 이러한 코드 통합 과정에서 사용되는 Git 명령어가 Merge 또는 Rebase이다. 두 명령어 모두 브랜치를 병합하는 데 사용되지만, Git 히스토리를 관리하는 방식이 다르다.

 

 

3. Git Merge

Merge는 병합하려는 브랜치들의 상황에 따라 2가지 방식 중 하나의 방식을 선택해 병합을 진행한다.

 

Fast-forward Merge

 

브랜치 간 병합 시 현재 브랜치의 HeadCommit 과 병합하려는 브랜치의 Base Commit이 일치할 경우, 현재 브랜치의 Head Commit이 병합하려는 브랜치의 Head Commit으로 이동되는 병합 방식이다.

 

Main 브랜치와 A 브랜치를 병합하는 경우를 생각해보자. Main 브랜치의 Head Commit은 M-C2이고, A 브랜치의 Base Commit도 M-C2이다. Main 브랜치에서 A 브랜치를 병합할 경우 Main 브랜치의 Head commit이 A 브랜치의 Head Commit인 A-C2로 이동하는 Fast-forward 방식의 병합이 일어난다.

 

 

 

 

3-way Merge
브랜치 간 병합 시 현재 브랜치의 HeadCommit 과 병합하려는 브랜치의 Base Commit이 일치하지 않을 경우, Merge commit을 생성하며 두 브랜치를 병합하는 방식이다.

 

앞선 예시에서 Main 브랜치와 A 브랜치를 Fast-forward 방식으로 병합했다. 이후 Main 브랜치와 B 브랜치를 병합하려는 상황을 보자. 현재 Main 브랜치의 Head Commit은 A-C2이고, B 브랜치의 Base Commit은 M-C2로 두 Commit이 일치하지 않는다. 이러한 경우 3-way 방식으로 두 브랜치가 병합된다. 새로운 Merge Commit이 생성되며, Git 히스토리에 Merge Commit이 남게 된다.

 

다른 예시로 Head Commit이 M-C3인 Main 브랜치와 Base Commit이 Merge Commit인 C 브랜치를 병합하는 상황을 생각해보자. Main 브랜치의 Merge Commit을 Base Commit으로 하는 C 브랜치에서 C-C1과 C-C2 Commit이 생성되는 사이에 Main 브랜치에서 M-C3 Commit이 생성되었다. Main 브랜치의 Head Commit이 M-C3으로 변경되어 C 브랜치의 Base Commit과 일치하지 않아 3-way 방식의 Merge가 진행된다.

 

 

 

Fast-forward 방식에선 브랜치 Commit에서 conflict가 발생하지 않지만, 3-way 방식에서는 conflict가 발생한다. 두 브랜치의 공통 조상 Commit을 기준으로 각 브랜치에서 이후 생성된 Commit 내역을 통해 수정된 부분을 Git이 확인한 후, 브랜치 중 한 쪽에서만 수정이 일어났다면 해당 부분은 병합 시 변경 사항을 반영한다. 하지만, 두 브랜치 모두에서 수정된 부분은 어떤 수정 사항을 반영해야 할 지 Git이 판단할 수 없어, conflict가 발생하게 된다. 이렇게 conflict가 발생하면 우리가 직접 어떤 수정 사항을 병합 시 반영할 것인 지 결정해야 하는 것이다. 이러한 conflict를 해결하면 Merge Commit이 생성되며 두 브랜치가 병합된다.

 

 

Git 히스토리의 경우, Commit이 두 브랜치로 관리되다가 Merge 시점 이후 하나로 합쳐진다. 즉, Git Merge를 사용하면 Git 히스토리를 통해 두 브랜치가 분기된 시점과 병합된 시점을 한눈에 알 수 있어, 변경 지점을 확인하기 용이하다.

 

 

4. Git Rebase

브랜치의 Base Commit을 재설정하는 명령어이다. A 브랜치에서 B 브랜치를 Rebase 할 경우 A 브랜치의 Base Commit이 B 브랜치의 Head Commit으로 변경된다. 이후 두 브랜치를 Merge하면 A 브랜치의 Base Commit이 B 브랜치의 Head Commit이 되어 Fast-forward 방식으로 병합될 수 있고, Git 히스토리를 깔끔하게 유지할 수 있다.

 

Git Merge vs Git Rebase

 

Merge와의 차이점을 확인하기 위해 예시를 들어보자. Main 브랜치의 M-C2를 Base Commit으로 하는 D 브랜치와 C 브랜치가 생성되고, 각 브랜치에서 작업이 진행되는 상황이다. D 브랜치에서는 D-C1, D-C2 Commit을 생성한 후, Main 브랜치와 Merge를 시도한다. 

 

 

Main 브랜치의 Head Commit과 D 브랜치의 Base Commit이 M-C2로 일치하여 Fast-forward 방식이 선택되었다.

 

다음으로 E 브랜치도 E-C1 Commit을 생성한 후, Main 브랜치로의 Merge를 시도한다. Main 브랜치의 Head Commit이 D-C2로 변경되어 E 브랜치의 Base Commit인 M-C2와 달라지게 되고, 3-way 방식으로 병합될 것이다.

 

Merge 방식으로 브랜치를 병합하면 최종적으로 위와 같은 Git 히스토리가 남는다.

 

이번엔 Rebase를 사용한다고 해보자. Main 브랜치의 Merge Commit을 Base Commit으로 F, G, H 브랜치가 생성되고 각 브랜치에서 작업을 진행하는 상황이다. Merge 방식으로 세 브랜치를 Main 브랜치에 병합한다고 하면, 처음에 병합한 브랜치는 Fast-forward 방식으로 병합될 것이고 나머지 두 브랜치는 3-way 방식으로 병합되어 Git 히스토리에 남게 될 것이다.

 

F 브랜치에서 Main 브랜치를 Rebase 한다면, F 브랜치의 Base Commit이 Main 브랜치의 Head Commit으로 변경되는 작업이 일어난다. 하지만, 이미 F 브랜치의 Base Commit이 Main 브랜치의 Head Commit인 Merge Commit이므로 별 다른 변경 사항이 없다.

이후 Main 브랜치에서 F 브랜치를 Merge 하면 Fast-forward 방식으로 두 브랜치가 병합된다.

 

Main 브랜치와 F 브랜치의 병합으로 Main 브랜치의 Head Commit이 F-C3으로 변경되었다. 이 상황에서 H 브랜치가 Main 브랜치를 Rebase 한다. 그렇다면 H 브랜치의 Base Commit이 Merge Commit에서 F-C3으로 변경될 것이다. 

이후 Main 브랜치에서 H 브랜치를 Merge 하면 앞에서와 같은 Fast-forward 방식으로 두 브랜치가 병합된다.

마찬가지로 G 브랜치가 Main 브랜치를 Rebase 하면, G 브랜치의 Base Commit이 H-C2로 변경되고 이후 Main 브랜치가 G 브랜치를 Merge 하면 두 브랜치는 Fast-forward 방식으로 병합될 수 있다.

 

결국 F, G, H 브랜치에서 작업된 Commit 내역이 마치 하나의 Main 브랜치에서 작업된 것처럼 Git 히스토리에 남게된다. 어느 지점에서 브랜치가 나뉘어서 작업되었는 지를 파악하기 어렵다. 일부 Git 히스토리를 생략하게 될 수도 있다는 단점도 있다.

 

 

5. Git Merge와 Git Rebase, 프로젝트에 적합한 것은?

둘 중 무엇을 쓸 지를 고민해보았다. Git 히스토리를 깔끔하게 정리하고 싶다면 Rebase를 함께 사용하고, 브랜치별 작업 내역과 변경 지점을 명확하게 알고 싶다면 Merge만 사용하는 것이 좋을 것 같다. 

 

로컬 브랜치에서 '혼자' 작업하면서 임시로 많은 브랜치를 만들어 작업한 후 원격 저장소에 Merge 하고 싶은 경우엔, Rebase를 사용해 Git 히스토리를 깔끔하게 정리할 수도 있겠다.

 

결과적으로 우리 팀원들과의 협업에서는 Rebase 사용은 지양하고, Merge만을 사용하는 것이 좋을 것 같다는 생각이 든다. 실무에서 Rebase를 활용해 Git 히스토리를 정리하는 경우가 많다고는 하지만, Rebase는 강제 Push의 위험성을 안고 있다. 팀원들 모두 Git에 대한 이해도가 높다면 Rebase를 함께 사용하는 것도 좋은 방법이지만, 이번 프로젝트에서는 Merge 사용을 권장해보는 것으로 해야겠다.

 

'ETC' 카테고리의 다른 글

도메인 주도 설계 (Domain-Driven Design; DDD)  (0) 2025.03.10