콘텐츠로 이동

브랜치와 병합

브랜치 개념을 이해하고 브랜치를 생성하고 병합하는 기본적인 시나리오와 방법을 배운다.

실습 안내

  • 이번 챕터는 실습과 함께 진행 됩니다.
  • 교재 내용을 따라가면서 실습으로 표시된 부분을 함께 진행 하도록 합니다.
  • 맥은 터미널, 윈도우는 Git Bash를 사용합니다.

브랜치

브랜치(branch)는 사전적 의미로 나무의 가지, 분기점이라는 뜻을 가지고 있다. git에서 브랜치는 코드를 분리하고 독립적으로 개발할 수 있도록 하는 일종의 복제된 작업공간으로 기존의 작업 내용을 베이스로 새로운 작업을 시작할 수 있도록 해준다. 이를 통해 여러 기능을 동시에 개발하거나 실험적인 변경사항을 현재 운영중인 코드에 영향을 주고 테스트할 수 있을 뿐 아니라 협업도 가능하다.

  • 브랜치는 기본 브랜치인 main에 영향을 주지 않으므로 안전하게 작업할 수 있다.
  • 즉, 브랜치에서 추가/수정/삭제 되는 모든 작업은 main에 영향을 주지 않기 때문에 필요한 기능을 테스트 하거나 기존 코드를 업그레이드 할 때 유용하다.
  • 브랜치 작업 과정에서 문제가 발생하면 다시 원래 브랜치로 돌아가고 해당 브랜치는 그냥 삭제하면 된다.
  • 대규모 프로젝트에서 수정 작업을 진행할 때 발생할 수 있는 문제점을 브랜치를 통해 해결할 수 있다.
  • 특히 여러 개발자가 역할을 나눠 작업하는 경우 각각 담당하는 기능을 별도의 브랜치로 작업하고 이를 병합하는 과정으로 통합할 수 있다.

다음은 실제 개발에서 브랜치를 활용하는 대표적인 시나리오이다.

브랜치 활용 시나리오

  • 신규 기능 개발 (Feature Branch)
  • 상황: "장바구니 담기" 기능을 만들어야 합니다.
  • 흐름: develop 브랜치에서 feature/cart 브랜치를 생성하여 작업합니다.
  • 작업이 완료되면 Pull Request(PR)를 생성하여 팀원들에게 코드 리뷰를 받은 뒤 develop에 합칩니다.

브랜치 생성

지금까지는 git 저장소의 기본 브랜치인 main에서 작업을 진행했다. 이번에는 새로운 브랜치를 생성하고 작업을 진행해보자. 먼저 현재 브랜치를 확인해 본다.

$ git branch
* main

Note

  • 실습은 03.Git 첫걸음 실습에 이어서 진행 한다.

브랜치 생성은 git branch 명령어를 사용한다. 브랜치 이름은 feature로 하고 생성해보자.

$ git branch feature
$ git branch
  feature
* main
  • 새로운 브랜치를 생성한다고 해서 현재 브랜치가 변경되지 않는다.
  • feature 브랜치가 생성되었으나 현재 작업중인 브랜치는 main이다.(* 로 확인)

브랜치 전환은 checkout 혹은 switch 명령을 사용한다.

  • checkout: git의 전통적인 브랜치 관리 명령어로 브랜치 전환, 파일 체크아웃, 커밋으로 돌아가기 등 다양한 용도로 사용된다.
  • switch: checkout의 모호성과 기능 복잡성으로 인해 브랜치 전환만 담당하는 새로운 명령어이다.
$ git switch(or checkout) feature
'feature' 브랜치로 전환합니다

$ git branch
* feature
  main
  • git branch 명령어로 현재 브랜치를 확인해보면 feature 브랜치로 전환된 것을 확인할 수 있다.
  • checkout을 사용하는 경우 출력 메시지가 다르다.(커밋 관련 정보가 포함됨)

새로운 브랜치를 생성하면서 바로 이동하는 경우에는 다음과 같이 할 수 있다.

$ git switch -c new-feature
or
$ git checkout -b new-feature

그러면 이제 feature 브랜치에서 작업을 진행한다고 가정해 보자.

브랜치 작업 시나리오

  • 2.txt 추가
  • 1.txt 수정

시나리오에 따라 다음과 같이 작업을 진행한다. 진행하기에 앞서 반드시 현재 브랜치가 feature인지 확인하도록 한다.

$ touch 2.txt
$ echo "world" >> 1.txt
$ cat 1.txt
hello
world
  • touch 명령으로 2.txt 파일을 생성한다.
  • echo 명령으로 1.txt 파일에 "world" 문자열을 추가한다.(이전 실습에서 기존 파일을 첫번째 라인에 hello가 들어가 있었음)
  • cat 명령으로 확인하면 1.txt 에 hello와 world가 순서대로 들어가 있는 것을 확인할 수 있다.

변경된 내용을 스테이징하고 커밋한다.

$ git add .
$ git commit -m "Add 2.txt and modify 1.txt"

원격 저장소 push

이제 로컬 저장소의 변경사항들을 원격 저장소로 전송한다.

$ git push origin feature
$ git branch -a
* feature
  main
  remotes/origin/feature
  remotes/origin/main
git branch -a 명령어로 현재 브랜치와 원격 저장소의 브랜치를 확인할 수 있다.

깃허브 웹에 접속해 보면 리파지토리에는 새로운 브랜치가 생성되어 있는 것을 확인할 수 있으며 compare & pull request 버튼을 클릭하여 브랜치를 병합하는 과정을 웹 상에서 진행할수도 있다.

Sponsored

병합

병합이란?

병합(merge)은 두개의 브랜치를 하나로 합치는 작업을 의미한다.

이제 main, feature 두개의 브랜치가 있고 main은 수정사항이 없이 유지되고 있으며 feature 브랜치만 새롭게 작업이 추가 되었다. 양쪽 모두 변경되는 경우 충돌(동일 파일이 원격, 로컬에서 서로 다른 작업(자)에 의해 변경됨) 문제를 처리하고 진행해야 한다.

여기서는 단순히 feature 브랜치에서 작업한 내용을 main 브랜치로 병합하는 방법을 알아보자.

보통 feature 에서 작업한 내용으로 테스트까지 완료되고 이상이 없다고 판단되면 다시 main 브랜치로 병합하는 과정을 거친다.

이러한 상황에서의 병합 방법은 다음과 같이 로컬에서의 작업을 중심으로 진행한다. 깃허브에서의 병합은 pull request를 통해 진행하는데 이 부분은 다음 챕터에서 다루도록 한다.

실제 협업 과정에는 여러 병합 유형이 발생할 수 있는데 다음은 대표적인 병합 유형이니 참고한다.

병합 전략 설명 명령어
Fast-forward 병합 대상 브랜치가 병합하려는 브랜치의 부모 커밋인 경우, 병합 커밋 없이 HEAD를 이동. 브랜치가 합쳐진 흔적이 남지 않음. 자동 동작 (옵션 필요 없음)
No Fast-forward Fast-forward를 방지하고 병합 커밋을 생성. 브랜치 간 작업 흐름을 명확히 구분. git merge --no-ff <branch>
Recursive (3-way merge) 두 브랜치가 공통 조상을 가지지 않는경우 Git이 공통 조상을 찾아 3-way 병합을 수행. 기본 병합 전략으로 사용. 자동 동작 (옵션 필요 없음)
Squash Merge 병합하려는 브랜치의 모든 커밋을 하나의 커밋으로 압축하여 병합. 히스토리 단순화에 유리. 브랜치의 히스토리는 남기지 않음. git merge --squash <branch>
Rebase 병합 대신 브랜치의 커밋을 다른 브랜치 위로 재배치. 히스토리를 선형으로 유지. fast-forward 병합 조건 만듬. git rebase <branch>
Octopus 3개 이상의 브랜치를 동시에 병합. 충돌이 없는 경우에만 사용. 단, 충돌이 없는 경우에만 가능. git merge <branch1> <branch2> <branch3>
Ours 병합 시 병합하려는 브랜치의 변경 사항을 무시하고 현재 브랜치의 내용을 유지. git merge -s ours <branch>
Manual Merge 충돌 발생 시 수동으로 충돌을 해결한 후 병합 커밋 생성. 충돌 해결 후 git commit

여기서는 Fast-forward 방식으로 병합을 진행한다. 진행 절차는 다음과 같다.

병합 절차

  1. main 브랜치로 이동
  2. main 브랜치에서 feature 브랜치를 병합
  3. main 브랜치를 원격 저장소로 push

협업 시 주의사항

  • 여기서는 협업 상황이 아닌 개인 작업 환경에서의 병합을 다루고 있다.
  • 협업 상황에서는 로컬에서 브랜치를 병합하기 전에 먼저 Pull Request(PR)를 생성하여 팀원들의 코드 리뷰를 받고 승인을 받은 후에 원격에서 먼저 병합하고 로컬에서 pull을 통해 업데이트 하는 것이 일반적이다.

병합 절차

0. 상태 확인

원격 저장소의 변경사항이 있는지 확인하고 현재 상태를 확인한다.

$ git fetch origin
$ git status
1. main 브랜치로 이동

원격에서의 변경사항이 없다면 main 브랜치로 이동하고 feature 브랜치를 병합한다.

브랜치를 병합할 때 병합 대상 브랜치(예 main)가 병합하려는 브랜치(예 feature)의 부모 커밋인 경우 Fast-forward 방식으로 병합된다. 이 경우, 별도의 병합 커밋을 생성하지 않고 단순히 병합 대상 브랜치의 HEAD를 병합하려는 브랜치의 HEAD로 이동시키는 방식이다.

Fast-forward 특징

  • 병합 커밋 없음: Fast-forward 병합은 별도의 병합 커밋을 생성하지 않는다.
  • 히스토리 단순화: 브랜치 히스토리가 깔끔하게 유지된다.
  • 조건: 병합 대상 브랜치가 병합하려는 브랜치의 모든 변경 사항을 포함하고 있어야 한다.

Fast-forward 단점

  • 브랜치 간의 작업 흐름을 명확히 구분하기 어렵게 만들 수 있다.
  • 협업시에 문제가 될 수 있으며 이경우 no-ff 옵션을 사용하여 병합 커밋을 생성할 수 있다.
$ git switch main
$ git merge feature
업데이트  d5bc996..c70af81
Fast-forward
 1.txt | 1 +
 2.txt | 0
 2 files changed, 1 insertion(+)
 create mode 100644 2.txt

$ ls
1.txt       2.txt       README.md
  • main 브랜치에는 원래 1.txt, README.md 파일이 있던 상태에서 2.txt 파일이 새로 생성됨.
  • 1.txt 파일은 수정된 것을 확인할 수 있다.
3. 원격 저장소로 push

마지막으로 main을 원격 저장소로 push 한다.

로컬에서 병합이 완료 되었으면 이를 원격 저장소로 push 할 수 있다. 이때 원격 저장소의 main 브랜치에 병합된 내용이 반영되므로 원격 저장소의 main 브랜치도 최신으로 업데이트 된다.

git log --oneline --graph 명령어로 히스토리를 확인해보면 병합된 내용을 시각적으로 확인할 수 있다. 병합된 커밋은 Merge branch 'feature'로 표시된다.

$ git push origin main
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:albks-edu/git-start.git
   d5bc996..c70af81  main -> main

$ git log --oneline --graph
*   e9007d7 (HEAD -> main, origin/main) Merge branch 'feature'
|\  
| * 44ee677 (feature) add 6.txt
* | 2f03bb1 add 5.txt
|/  
* 8ac85a6 add 3.txt, 4.txt
* c70af81 Add 2.txt and modify 1.txt
* d5bc996 Create README.md
* 179aef3 삭제
* 5744d33 Delete 2.txt
* 1419b54 Create 2.txt
* 358f483 Create README.md
* 178dc10 1.txt 수정
* 6b8cc39 1.txt 추가

log 보는 법

    - 아래에서 부터 위로 갈수록 최신 커밋이다.
    - `*` : 커밋을 나타낸다.
    - `|` : 브랜치의 흐름을 나타낸다.
    - `\` : 브랜치가 합쳐지는 지점을 나타낸다.
    - `Merge branch 'feature'` : feature 브랜치가 main 브랜치에 병합된 커밋임을 나타낸다.
  • 20-12: main 브랜치의 커밋
  • 10-9: feature 브랜치의 커밋
  • 7: feature 브랜치가 main 브랜치로 병합된 커밋
4. 브랜치 삭제

병합이 완료되면 feature 브랜치는 더이상 필요하지 않으므로 삭제한다.

깃허브에서 확인해 보면 main 브랜치가 머지된 상태로 변경된 것을 확인할 수 있다. 아직 feature 브랜치는 존재하고 있으며 더이상 필요없는 브랜치라면 관리를 위해 삭제하는 것이 좋다. 로컬과 원격 저장소에서 모두 삭제하는 방법은 다음과 같다. 작업은 main 브랜치에서 해야 한다.

# 로컬 브랜치 삭제
$ git branch -d feature
feature 브랜치 삭제 (과거 c70af81).

# 원격 브랜치 삭제
$ git push origin --delete feature
To github.com:albks-edu/git-start.git
 - [deleted]         feature

Sponsored

리베이스

리베이스란?

리베이스(rebase)는 일종의 merge 전략 중 하나로 활용할 수 있는 방법으로 브랜치의 커밋들을 다른 브랜치 위로 재배치하는 작업을 의미한다. 리베이스는 병합과 달리 히스토리를 선형으로 유지할 수 있어 깔끔한 커밋 히스토리를 원하는 경우에 유용하다.

리베이스의 기본 개념과 사용법은 다음과 같다.

  • 리베이스는 브랜치의 커밋들을 다른 브랜치 위로 재배치하는 작업이다.
  • 이를 통해 브랜치 간의 변경 사항을 통합할 수 있으며, 히스토리를 선형으로 유지할 수 있다.
graph
    subgraph "Rebase 후"
    A2((C1)) --- B2((C2)) --- C2((C3)) --- D2((C4')) --- E2((C5'))
    end

    subgraph "Rebase 전"
    A((C1)) --- B((C2)) --- C((C3))
    B --- D((C4)) --- E((C5))
    end
    style A fill:#f9f,stroke:#333,stroke-width:2px
리베이스 사용법
  • 리베이스는 git rebase <branch> 명령어를 사용하여 수행한다.
  • 예를 들어, feature 브랜치를 main 브랜치 위로 리베이스하려면 다음과 같이 한다.
$ git switch feature
$ git rebase main
  • 리베이스 과정에서 충돌이 발생할 수 있다.
  • 이 경우 수동으로 충돌을 해결한 후 git rebase --continue 명령어를 사용하여 리베이스를 계속 진행한다.

리베이스와 병합 비교

리베이스와 병합은 단순하게 비교할 수 있는 대상이라기 보다는 상황에 따라 적절한 방법을 선택하는 것이 중요하다. 다음 표는 리베이스와 병합의 차이점과 상황에 따른 권장 사용법을 정리한 것이다.

상황 Main의 상태 Rebase 필요 여부 병합 결과
A
(이상적)
내가 branch를 딴 이후 main에 추가 커밋 없음 불필요 바로 git mergeFast-forward 성공
B
(일반적)
내가 작업하는 동안 팀원이 main에 새로운 커밋을 올림 필요 rebase 없이 mergeMerge Commit 발생 (비선형)
C
(정석)
상황 B에서 히스토리를 깔끔하게 남기고 싶을 때 권장 rebasemerge 하여 Fast-forward 강제 수행

실습

브랜치 생성, 병합, 리베이스를 실습해 본다.

실습 안내

  • 새로운 폴더를 생성하고 git 저장소를 초기화 한다.
  • 단계별로 파일을 추가하고 커밋을 진행한다.
  • 새로운 브랜치를 생성하고 작업을 진행한 후 병합과 리베이스를 실습해 본다.

1. 공통 작업 환경 만들기

먼저 실습을 위한 새로운 폴더를 생성하고 git 저장소를 초기화 한다. 이후 단계별로 파일을 추가하고 커밋을 진행한다. 폴더명은 다른 이름을 사용해도 무방하다.

git init rebase-test
cd rebase-test
echo "base" > base.txt && git add . && git commit -m "C1: Base"

# feature 브랜치 생성 및 작업
git checkout -b feature
echo "feat" > feat.txt && git add . && git commit -m "C2: Feature work"

# main 브랜치 이동 후 추가 작업
git checkout main
echo "main" > main.txt && git add . && git commit -m "C3: Main update"

2. 병합 실습

다음으로 main 에서 feature 브랜치를 병합하는 과정을 진행 한다.

# main 브랜치에서 feature를 병합
git merge feature -m "C4: Merge feature branch"

# 히스토리 확인
git log --oneline --graph --all
히스토리 결과
*   971d1bf (HEAD -> main) C4: Merge feature branch
|\  
| * d0ec8a1 (feature) C2: Feature work
* | 5c25cb7 C3: Main update
|/  
* 4ddc3b6 C1: Base

3. rollback

이제 rebse가 merge와 어떻게 다른지 비교하기 위해 merge 이전 상태로 롤백 한다.

# Merge 하기 전인 C3 커밋 상태로 강제 이동
git reset --hard HEAD~1

4. 리베이스 실습

# feature 브랜치로 이동하여 main 기준 재배치
git checkout feature
git rebase main

# 히스토리 확인
git log --oneline --graph --all
히스토리 결과
* 03aa352 (feature) C2: Feature work
* 5c25cb7 (HEAD -> main) C3: Main update
* 4ddc3b6 C1: Base
  • 리베이스 후 히스토리를 확인해 보면 feature 브랜치의 커밋이 main 브랜치의 커밋 뒤로 재배치된 것을 확인할 수 있다.
  • 즉, C2 커밋이 C3 커밋 뒤로 붙어 전체가 하나의 직선으로 정렬된 형태이다.

이제 rebase를 통해 커밋 히스토리가 feature와 main이 합친 형태로 변경되었음을 알 수 있다. 그러나 main 브랜치로 이동해서 ls -la 명령어로 파일 목록을 확인해 보면 아직 feat.txt 파일이 보이지 않는다. 이는 rebase가 단순히 커밋 히스토리를 재배치하는 작업이기 때문이다.

# main 브랜치로 이동
$ git checkout main
$ ls
base.txt    main.txt

$ git diff main feature
diff --git a/feat.txt b/feat.txt
new file mode 100644
index 0000000..c774709
--- /dev/null
+++ b/feat.txt
@@ -0,0 +1 @@
+feat
  • main 브랜치에는 feat.txt 파일이 존재하지 않는다.
  • git diff 명령어로 main과 feature 브랜치의 차이를 확인해 보면 feat.txt 파일이 새로 추가된 것을 알 수 있다.

최종적으로 feature 브랜치를 main 브랜치에 병합하면 작업이 마무리 된다.

# main 브랜치에서 feature 브랜치를 병합
$ git checkout main (앞에서 했다면 생략 가능)
$ git merge feature -m "C4: Merge feature branch after rebase"