Search

Pro Git 3장 - Git 브랜치 (2/2)

이번에는 리모트 브랜치의 개념과 리베이스 활용법, 그리고 효과적인 협업 전략까지 알아보겠습니다. Git을 활용한 팀 협업에서 이해하고 있는 것이 중요한 내용입니다.

1. 리모트 브랜치의 삼중 개념

Git으로 여러 개발자와 협업할 때는 리모트 브랜치 개념을 정확히 이해하는 것이 중요합니다. 일견 비슷해 보이는 세 가지 개념을 구분해보겠습니다.

리모트 Refs, 리모트 트래킹 브랜치, 업스트림 관계

1. 리모트 Refs(Remote References)
리모트 Refs는 원격 저장소에 있는 브랜치, 태그 등을 참조하는 포인터입니다. 이는 원격 저장소의 상태를 직접 가리키는 참조로, git ls-remote <remote> 명령으로 확인할 수 있습니다:
$ git ls-remote origin 452e4244c8a24f86efc800a99c53eb333cdcf1a9 HEAD 452e4244c8a24f86efc800a99c53eb333cdcf1a9 refs/heads/master a93c3025dd16060506dc17183f535661551c1487 refs/heads/feature
Shell
복사
이 명령은 실제로 원격 저장소에 쿼리를 보내 현재 상태를 조회합니다.
2. 리모트 트래킹 브랜치(Remote-tracking Branches)
리모트 트래킹 브랜치는 로컬에 저장된 리모트 브랜치의 상태를 나타내는 레퍼런스입니다. 예를 들어 origin/master, origin/develop 등의 형태로 명명됩니다. 이 브랜치들은 마지막으로 서버와 통신했을 때(주로 git fetch 또는 git pull 명령 실행 시) 리모트 브랜치가 어떤 상태였는지를 보여줍니다.
중요한 특징은 다음과 같습니다:
리모트 트래킹 브랜치는 로컬에 있지만 직접 수정할 수 없습니다.
원격 저장소와 통신할 때만 자동으로 업데이트됩니다.
.git/refs/remotes/<remote_name>/ 경로에 저장됩니다.
3. 업스트림 관계와 트래킹 브랜치(Tracking Branches)
트래킹 브랜치는 로컬 브랜치와 리모트 트래킹 브랜치 사이의 직접적인 연결 관계를 의미합니다. 이 관계가 설정된 로컬 브랜치를 가리켜 '트래킹 브랜치'라고 하며, 이때 트래킹 대상이 되는 리모트 브랜치를 '업스트림 브랜치'라고 합니다.
트래킹 브랜치에서는 git pull이나 git push 명령을 인자 없이 실행해도 Git이 어떤 리모트 브랜치와 작업해야 하는지 알 수 있습니다.

.git/refs/ 구조와 레퍼런스 시스템

Git 저장소의 .git/refs/ 디렉토리는 다양한 레퍼런스(브랜치, 태그, 리모트 등)를 저장하는 구조를 가지고 있습니다:
.git/refs/ |-- heads/ # 로컬 브랜치 | |-- master | |-- develop | `-- feature |-- tags/ # 태그 | `-- v1.0 `-- remotes/ # 리모트 트래킹 브랜치 `-- origin/ |-- HEAD |-- master `-- feature
Plain Text
복사
각 파일의 내용은 해당 레퍼런스가 가리키는 커밋의 SHA-1 해시값입니다. 이 구조를 이해하면 Git이 어떻게 다양한 참조를 관리하는지 명확히 알 수 있습니다.

리모트 브랜치 네이밍 규칙 (origin/master의 의미)

리모트 트래킹 브랜치는 <remote>/<branch> 형식으로 명명됩니다. 예를 들어, origin/masterorigin 리모트의 master 브랜치를 가리키는 로컬 레퍼런스입니다.
여기서 origin은 특별한 의미가 있는 이름이 아니라, git clone 명령 실행 시 Git이 자동으로 생성하는 기본 리모트 이름입니다. 클론 시 -o 옵션을 사용하여 다른 이름을 지정할 수 있습니다:
$ git clone -o upstream https://github.com/user/repo.git
Shell
복사
이 경우 리모트 트래킹 브랜치는 upstream/master와 같은 형태가 됩니다.

2. 리모트 브랜치 작업 마스터하기

Push, Fetch, Pull의 내부 동작 차이

리모트 브랜치와 관련된 세 가지 주요 Git 명령은 각각 다른 방식으로 동작합니다:
1. git fetch
git fetch는 원격 저장소에서 데이터를 가져와 로컬의 리모트 트래킹 브랜치를 업데이트합니다:
$ git fetch origin
Shell
복사
이 명령은 다음 작업을 수행합니다:
원격 저장소의 모든 브랜치와 태그 정보를 가져옵니다.
로컬에 없는 커밋을 다운로드합니다.
리모트 트래킹 브랜치(예: origin/master)를 업데이트합니다.
작업 디렉토리나 로컬 브랜치는 변경하지 않습니다.
Fetch는 안전한 작업으로, 현재 작업 중인 내용에 영향을 주지 않고 원격 변경사항을 확인할 수 있습니다.
2. git pull
git pullgit fetchgit merge(또는 git rebase)를 연속해서 실행하는 것과 같습니다:
$ git pull origin master
Shell
복사
이 명령은 다음 작업을 수행합니다:
1.
원격 저장소에서 데이터를 가져와 리모트 트래킹 브랜치를 업데이트합니다(fetch).
2.
현재 브랜치와 업데이트된 리모트 트래킹 브랜치를 병합합니다(merge).
-rebase 옵션을 사용하면 병합 대신 리베이스를 수행합니다:
$ git pull --rebase origin master
Shell
복사
3. git push
git push는 로컬 브랜치의 변경사항을 원격 저장소에 업로드하고, 원격 브랜치를 업데이트합니다:
$ git push origin feature
Shell
복사
이 명령은 다음 작업을 수행합니다:
로컬 브랜치의 커밋 중 원격에 없는 것들을 업로드합니다.
원격 브랜치 레퍼런스를 업데이트합니다.
성공적으로 Push한 후에는 로컬의 리모트 트래킹 브랜치도 자동으로 업데이트됩니다.

브랜치 추적 관계 설정하기

브랜치 추적 관계는 여러 방법으로 설정할 수 있습니다:
1. 리모트 브랜치를 체크아웃할 때 자동 설정
$ git checkout -b feature origin/feature
Shell
복사
또는 Git 1.6.2 이상에서는 더 간단한 방법도 가능합니다:
$ git checkout --track origin/feature
Shell
복사
Git 2.23 이상에서는 git switch 명령을 사용할 수도 있습니다:
$ git switch --track origin/feature
Shell
복사
2. 기존 브랜치에 추적 관계 설정
이미 존재하는 로컬 브랜치에 추적 관계를 설정하려면:
$ git branch -u origin/feature feature
Shell
복사
또는 현재 브랜치의 추적 관계를 설정하려면:
$ git branch -u origin/feature
Shell
복사
3. 추적 관계 확인
현재 설정된 추적 관계를 확인하려면:
$ git branch -vv master 452e424 [origin/master] Initial commit * feature a93c302 [origin/feature: ahead 2, behind 1] Add new feature
Shell
복사
이 출력은 feature 브랜치가 origin/feature를 추적하고 있으며, 로컬에 원격에 없는 커밋이 2개(ahead 2), 원격에 로컬에 없는 커밋이 1개(behind 1) 있음을 보여줍니다.

리모트 브랜치 삭제와 관리

1. 리모트 브랜치 삭제
작업이 완료된 리모트 브랜치는 다음 명령으로 삭제할 수 있습니다:
$ git push origin --delete feature
Shell
복사
2. 로컬 리모트 트래킹 브랜치 정리
원격 저장소에서 이미 삭제된 브랜치에 대한 리모트 트래킹 브랜치를 정리하려면:
$ git fetch --prune
Shell
복사
또는 fetch 명령을 실행할 때마다 자동으로 정리하도록 설정:
$ git config --global fetch.prune true
Shell
복사
3. 특정 리모트 브랜치만 가져오기
특정 브랜치만 가져오려면 다음과 같이 지정할 수 있습니다:
$ git fetch origin master:refs/remotes/origin/master
Shell
복사
이는 원격의 master 브랜치만 가져와 로컬의 origin/master 리모트 트래킹 브랜치를 업데이트합니다.

3. Rebase의 내부 동작과 마법

Patch 기반 변경사항 처리 방식

Rebase는 커밋을 "재배치"하는 Git의 강력한 기능입니다. 내부적으로는 다음과 같은 과정으로 작동합니다:
1.
현재 브랜치와 대상 브랜치의 공통 조상을 찾습니다.
2.
현재 브랜치에서 이 공통 조상 이후의 모든 커밋에 대한 변경사항(diff)을 추출하여 임시 저장합니다.
3.
현재 브랜치를 대상 브랜치의 최신 커밋으로 이동시킵니다.
4.
저장해둔 변경사항을 순서대로 적용하여 새 커밋을 생성합니다.
이 과정에서 Git은 각 커밋의 변경사항을 패치(patch) 형태로 추출하여 처리합니다. 패치란 파일에 대한 변경 사항을 담은 텍스트 형식의 데이터로, 어떤 라인이 추가되고 삭제되었는지를 포함합니다.

Rebase 시 내부적으로 일어나는 일

예를 들어, feature 브랜치를 master 브랜치 위로 리베이스하는 과정을 살펴보겠습니다:
$ git checkout feature $ git rebase master
Shell
복사
내부적으로 다음과 같은 일이 발생합니다:
1.
Git은 featuremaster의 공통 조상을 찾습니다.
2.
이 공통 조상부터 feature의 현재 커밋까지의 모든 커밋에 대한 변경사항을 추출하여 .git/rebase-apply/ 또는 .git/rebase-merge/ 디렉토리에 임시 저장합니다.
3.
feature 브랜치의 참조를 master의 최신 커밋으로 이동시킵니다.
4.
임시 저장된 각 패치를 순서대로 적용하여 새 커밋을 생성합니다.
이 과정에서 원본 커밋의 SHA-1 해시는 변경되지만, 변경 내용 자체는 유지됩니다. 결과적으로 feature 브랜치는 master 브랜치의 최신 상태를 기반으로 하면서도 자신만의 변경사항을 유지하게 됩니다.

Patch-ID 개념과 중복 변경 감지 메커니즘

리베이스 과정에서 중요한 개념 중 하나는 "patch-id"입니다. Patch-ID는 변경 내용 자체에 대한 고유 식별자로, 커밋 메타데이터(작성자, 커밋 날짜 등)가 아닌 순수하게 "무엇이 변경되었는가"에 기반하여 계산됩니다.
Git은 이 patch-id를 활용하여 다음과 같은 기능을 제공합니다:
1.
중복 커밋 감지: 리베이스 과정에서 이미 적용된 변경사항(예: cherry-pick된 커밋)을 다시 적용하지 않습니다.
2.
리베이스 충돌 해결 후 재개: 충돌이 발생한 패치를 건너뛰지 않고 정확히 식별하여 처리합니다.
3.
협업 시 중복 작업 감지: 서로 다른 개발자가 독립적으로 동일한 변경을 수행한 경우, 병합 과정에서 이를 자동으로 처리할 수 있습니다.
이런 메커니즘 덕분에 리베이스는 복잡한 상황에서도 일관되고 예측 가능한 결과를 제공할 수 있습니다.

4. Rebase vs Merge: 언제 무엇을 선택할 것인가

히스토리 관점의 차이: 기록 vs 이야기

Git 히스토리를 바라보는 두 가지 관점에 따라 Merge와 Rebase 중 어떤 전략을 선택할지 결정할 수 있습니다:
1. 히스토리를 "작업한 내용의 기록"으로 보는 관점
이 관점에서는 히스토리가 "실제로 무슨 일이 있었는지"를 정확히 보여주어야 합니다. 따라서:
커밋 히스토리를 변경하는 것은 "역사를 부정하는" 행위로 간주됩니다.
병합 커밋을 포함한 복잡한 그래프 구조도 실제 작업 흐름을 반영하는 것이므로 가치가 있습니다.
Merge를 선호하는 접근법입니다.
2. 히스토리를 "프로젝트가 어떻게 진행되었나에 대한 이야기"로 보는 관점
이 관점에서는 히스토리가 "프로젝트가 어떻게 만들어졌는지"를 명확하게 설명해야 합니다:
후대에 다른 개발자들이 이해하기 쉽도록 히스토리를 정리하는 것이 중요합니다.
불필요한 복잡성이나 중간 작업 단계는 최종 결과물을 이해하는 데 방해가 될 수 있습니다.
Rebase를 선호하는 접근법입니다.
두 관점 모두 유효하며, 프로젝트나 팀의 상황에 따라 적절한 전략을 선택해야 합니다.

각 접근법의 장단점 분석

Merge의 장점:
실제 작업 흐름과 이력을 그대로 보존합니다.
원본 커밋의 컨텍스트와 시간 정보가 유지됩니다.
비파괴적 작업이므로 위험성이 낮습니다.
대규모 팀 협업에 더 안전합니다.
Merge의 단점:
많은 병합이 발생하면 히스토리가 복잡해져 이해하기 어려울 수 있습니다.
기능별 개발 흐름을 추적하기 어려울 수 있습니다.
불필요한 merge 커밋이 많아질 수 있습니다.
Rebase의 장점:
선형적이고 깔끔한 히스토리를 제공합니다.
각 기능의 개발 과정을 명확하게 볼 수 있습니다.
불필요한 중간 단계를 제거하여 히스토리를 간소화할 수 있습니다.
Fast-forward 병합이 가능해져 병합 커밋 없이 브랜치를 통합할 수 있습니다.
Rebase의 단점:
커밋 해시가 변경되어 이미 공개된 브랜치에 적용하면 문제가 발생할 수 있습니다.
대규모 리베이스에서 충돌 해결이 복잡할 수 있습니다.
원본 커밋의 정확한 시간 순서가 손실될 수 있습니다.
잘못 사용하면 팀 협업에 혼란을 가져올 수 있습니다.

5. Rebase의 규칙과 안전한 사용법

"공개 브랜치는 리베이스하지 마라"

Git 커뮤니티에서 널리 받아들여지는 가장 중요한 규칙 중 하나는 "이미 공개 저장소에 Push 한 커밋을 Rebase 하지 마라"입니다. 이 규칙을 지키지 않으면 다음과 같은 문제가 발생할 수 있습니다:
1.
협업 혼란: 다른 개발자가 이미 기존 커밋을 기반으로 작업하고 있다면, 그들의 브랜치와 리베이스된 브랜치 사이에 심각한 불일치가 발생합니다.
2.
중복 커밋 문제: 리베이스는 새로운 해시값을 가진 커밋을 생성하므로, 원본 커밋과 리베이스된 커밋이 실질적으로 동일한 변경사항을 나타내더라도 Git은 이를 별개의 변경으로 간주합니다.
3.
병합 지옥: 다른 개발자가 리베이스된 브랜치를 풀하고 자신의 작업과 병합하려 할 때, 복잡한 충돌과 중복 커밋이 발생할 수 있습니다.
이런 문제를 예방하기 위해 "공개 브랜치는 리베이스하지 않는다"는 원칙을 준수해야 합니다.

안전하게 리베이스할 수 있는 상황

다음과 같은 상황에서는 안전하게 리베이스를 사용할 수 있습니다:
1.
로컬에서만 존재하는 커밋: 아직 Push하지 않은 로컬 커밋은 자유롭게 리베이스할 수 있습니다.
2.
개인 기능 브랜치: 혼자만 사용하는 브랜치라면 리베이스를 통해 정리한 후 공유 브랜치에 병합할 수 있습니다.
3.
PR/MR 전 정리: 풀 리퀘스트나 머지 리퀘스트를 제출하기 전에 변경사항을 정리하는 용도로 사용할 수 있습니다.

리베이스 문제 해결 전략 (-rebase)

리베이스로 인한 문제가 발생했거나, 리베이스된 브랜치와 동기화해야 할 경우 다음과 같은 전략을 사용할 수 있습니다:
1. git pull --rebase 활용
원격 브랜치가 리베이스되었다면, 로컬 브랜치를 다음과 같이 동기화할 수 있습니다:
$ git pull --rebase origin branch-name
Shell
복사
이 명령은 로컬 변경사항을 임시 저장하고, 원격 브랜치의 변경사항을 적용한 후, 로컬 변경사항을 다시 적용합니다.
2. 브랜치 재설정 및 재구성
더 복잡한 상황에서는 브랜치를 완전히 재설정하고 다시 시작하는 것이 효과적일 수 있습니다:
# 로컬 변경사항 백업 $ git branch backup-branch # 브랜치 재설정 $ git fetch origin $ git reset --hard origin/branch-name # 필요한 경우 백업 브랜치에서 변경사항 cherry-pick $ git cherry-pick backup-branch~3..backup-branch
Shell
복사
3. git rebase --onto 활용
특정 범위의 커밋을 다른 기반으로 이동시키려면:
$ git rebase --onto new-base old-base branch-name
Shell
복사
이 명령은 old-basebranch-name 사이의 커밋을 new-base 위로 리베이스합니다.
4. Patch-ID를 활용한 자동 해결
Git은 patch-id를 통해 같은 변경사항을 식별할 수 있으므로, 이를 활용하여 리베이스 문제를 자동으로 해결하는 경우도 있습니다. git rebase teamone/master 명령을 실행하면 Git은 다음 작업을 수행합니다:
1.
현재 브랜치에만 포함된 커밋을 결정합니다.
2.
Merge 커밋이 아닌 것을 결정합니다.
3.
이 중 병합할 브랜치에 이미 적용된 커밋(동일한 patch-id를 가진)을 제외합니다.
4.
나머지 커밋을 대상 브랜치에 적용합니다.
이런 기능 덕분에 일부 리베이스 문제는 Git이 자동으로 해결할 수 있습니다.

결론

리모트 브랜치와 리베이스는 Git의 강력한 기능이지만, 그 복잡성 때문에 많은 개발자들이 충분히 활용하지 못하고 있습니다. 이 글을 통해 리모트 브랜치의 삼중 개념, 리베이스의 내부 동작, 그리고 효과적인 협업 전략까지 살펴보았습니다.
Git의 중요한 원칙 중 하나는 "공개된 브랜치는 리베이스하지 않는다"는 것입니다. 이 원칙을 지키면서도 리베이스를 효과적으로 활용하면, 깔끔한 프로젝트 히스토리와 원활한 협업을 동시에 달성할 수 있습니다.