Git merge / rebase / cherry-pick으로 히스토리 다루기

Tech/기타 2025. 11. 21. 18:24
728x90
728x90

 

 

 

 

 

[이전글] Git의 데이터 저장 방식과 commit 이해하기

[이전글] Branch와 HEAD로 보는 Git 히스토리 모델(DAG) 이해하기

 

 

 

서론

1,2편의 포커스는 Git 안에 무엇이 저장되고 그 위에 branch/HEAD가 어떤 히스토리 그래프를 만드는지였습니다.

이번 편에서는 이 히스토리 위에서 실제로 우리가 사용하는 merge/rebase/cherry-pick 명령어들이 DAG 위에서 어떻게 커밋을 합치고 다시 쓰는지를 정리해보려고 합니다.

 

 

사용할 예제 레포

이번 글에서는 새로운 레포를 하나 생성해서 사용하려고 합니다.

git init git-merge-rebase-demo
cd git-merge-rebase-demo

echo "console.log('hello');" > app.js
git add app.js
git commit -m "init: add app.js"

 

간단하게 새 디렉토리에서, app.js 파일 하나만 커밋했습니다. 저는 여기에서, 기능 하나를 branch에서 개발한다고 가정하고 두 개의 커밋을 추가해보겠습니다.

git switch -c feature/login

echo "function login() {}" >> app.js
git add app.js
git commit -m "feat: add empty login"

echo "function validateUser() {}" >> app.js
git add app.js
git commit -m "feat: add validateUser"

 

마지막으로, 이 상태에서 main branch에도 작업을 추가하겠습니다.

git switch main

echo "console.log('tracking...');" >> analytics.js
git add analytics.js
git commit -m "feat: add analytics"

 

 

 

 

 

merge

git merge 는 현재 branch에 다른 branch의 변경 사항을 통합하는 명령입니다. 독립적으로 진행된 branch들의 히스토리를 다시 한 줄로 합치는 역할을 합니다. 조금 더 풀어보면 기준이 되는 branch에서 다른 branch에 대한 merge 명령을 실행하면, Git은 두 branch의 공통 조상(merge-base)를 찾고 그 시점 이후의 변경사항을 합쳐 새로운 커밋(merge commit)을 만듭니다.

단 fast-forward가 가능한 경우는 제외되는데, 이는 바로 아래에서 다루겠습니다.

 

 

fast-forward

먼저, 가장 단순한 케이스부터 살펴보겠습니다.

아직 main에서 아무 작업을 하지 않은 상태에서 아래처럼 feature만 앞으로 나간 경우입니다.

 

 

이 상태에서 feature branch를 merge하면 Git은 main도 F2를 가리키게 만들면 되겠다 라고 단순하게 판단합니다.

이 때 새 커밋을 만들지 않고, branch 포인터만 앞으로 이동시키는데 이를 fast-forward라고 합니다. 뒤에서 볼 rebase와 cherry-pick은 공통적으로 다른 곳의 변경을 현재 브랜치 위에 가져온다는 점에서는 비슷하지만, fast-forward와 달리 새 커밋을 만들어서 적용한다는 차이가 있습니다.

 

 

 

 

3-way-merge

앞에서 만든 예제처럼 main과 feature/login이 서로 다르게 진행된 상태를 다시 보겠습니다.

 

 

이 상태에서 다시 feature/login을 merge를 실행해보면 다음과 같은 일들이 일어납니다.

  1. main과 feature/login의 공통 조상(merge-base)를 찾음 (M0)
  2. M0 → M1 사이의 변경과 M0 → F2 사이의 diff를 비교
  3. 둘을 합쳐 새로운 커밋(merge commit)을 생성
  4. main branch에서 새로운 커밋을 가리키도록 함

 

main branch에서 git merge feature/login 명령을 실행한 결과는 다음과 같은 형태가 됩니다.

 

여기서 새로운 커밋(MG)은 병합 대상이었던 두 커밋을 동시에 부모로 가지게 됩니다. DAG 관점에서는 두 갈래가 한 점으로 합쳐지는 노드가 생성되었습니다.

 

정리하면 merge 명령은 기준 branch(현재 HEAD)에서 합치고 싶은 다른 branch의 commit들을 가져와서 공통 조상 이후의 변경 내용을 통합해 새로운 commit을 만드는 명령입니다.

 

 

 

rebase

같은 예제로 rebase를 보겠습니다. 현재 히스토리는 아래처럼 갈라져 있습니다. M0을 기준으로 main도 새로운 커밋이 존재하고, feature/login 또한 M0을 기준으로 새로운 커밋들이 존재합니다.

 

 

git rebase 는 한 branch에서 만들어진 commit들을 다른 시작점으로 옮겨(transplant) 다시 적용하는 명령어입니다.

즉 branch의 base를 다른 commit으로 바꾸는 것처럼 보이게 만들며, 내부적으로는 새 commit들을 만들고 그 위에 다시 쌓는다는 것입니다. 더 쉽게말해 브랜치를 다른 시작점 위로 끌어올려서, 마치 거기서부터 시작한 것처럼 히스토리를 다시 쓰는 것입니다.

 

 

지금 상황에서 feature/login branch에서 main의 내용을 가져오고 싶어서 git rebase main 명령을 실행했다고 해봅시다. 이 때 rebase는 다음과 같이 동작합니다.

  1. feature/login에서 main에 없는 commit 목록을 찾음 (F1, F2)
  2. main의 최신 commit인 M1을 기준으로 F1, F2의 변경 내용을 순서대로 다시 적용하면서 새 commit을 생성
  3. feature/login branch ref를 예전 F2가 아니라 새로운 commit으로 이동

 

 

R1, R2는 F1, F2에서 했던 변경 내용을 main 최신 커밋(M1) 위에 다시 적용한 결과이기 때문에, 코드 관점에서는 같은 변경에 가깝지만 Git 입장에서는 해시, 부모 정보 등이 모두 다른 완전히 새로운 커밋입니다.
 feature/login branch는 이제 R2를 가리키게 되어 F2에 대한 참조가 끊어지게 됩니다. 하지만 1편에서 언급했던 것 처럼 Git의 저장소는 읽기, 쓰기만 가능하기 때문에 참조되지 않는 F1, F2 커밋도 .git/objects와 reflog에 그대로 남아있게 됩니다.

 

 

 

merge vs rebase

같은 상황에서 merge, rebase를 비교해봤습니다.

 

merge는 새 merge commit을 추가해서 히스토리를 합치고 분기/병합 구조가 그래프에 그대로 남기 때문에 타임라인을 보존하고 싶을 때 유리합니다.

 

하지만 rebase는 특정 branch의 새로운 커밋들을 다시 만들어서 다른 branch의 HEAD commit 뒤에 이어 붙입니다. 이전 commit은 더이상 참조되지 않기 때문에 외형상 한 줄짜리 깔끔한 히스토리가 됩니다. 대신 기존 commit의 ID(hash)가 모두 바뀐다는 점을 주의해야합니다.

 

 

 

 

cherry-pick

cherry-pick은 특정 commit만 가져올 때 사용하는 명령입니다. 한 branch 내에 단일 commit 혹은 여러 commit들을 다른 branch의 최신 commit 위에 추가합니다. 앞에서 봤던 merge/rebase가 branch 단위로 여러 commit을 통째로 옮기는 느낌이라면, cherry-pick는 원하는 commit만 골라 복사하는 명령에 가깝습니다.

 

조금 단순한 예제를 하나 더 가정해보겠습니다. 이번에는 main에서 hotfix/log branch를 하나 생성하여 두 개의 hotfix commit을 만들었습니다. H1은 중요한 버그 픽스라 main에도 바로 반영되어야하고, H2는 단순 디버깅 로그라 main에는 바로 반영하지 않아도 된다고 가정하겠습니다.

git switch main
git switch -c hotfix/log

echo "console.log('fix null');" >> analytics.js
git add analytics.js
git commit -m "fix: handle null in analytics"

echo "console.log('extra debug');" >> analytics.js
git add analytics.js
git commit -m "chore: add extra debug log"

 

 

 

git switch main
git cherry-pick H1

 

git cherry-pick 을 실행하면 Git은 H1의 변경 내용을 기준으로 현재 main이 가리키는 M1 위에 새 commit H1'을 하나 더 만듭니다.

 

 

 

main 입장에서는 새 commit이 하나 생성된 것입니다. H1과 내용은 같지만 다른 commit hash를 가진 별도의 커밋이 됩니다.

 

cherry-pick을 과하게 사용하면 내용이 같지만 해시가 다른 커밋들이 여러 군데 생겨서 히스토리 추적이 힘들어질 수 있을 것 같습니다. 그래서 보통은 지금 예시처럼 hotfix 일부만 main에 반영해야 할 때나, 잘못된 브랜치에 커밋했을 때 특정 커밋만 옮기고 싶을 때 정도에 사용하는 편이 좋다고 느꼈습니다.

 

 

 

정리

Git의 내부 동작 원리를 세 편으로 나눠서 정리해봤습니다.

 

1편에서는 Git의 내부 저장소를 해시 기반 K-V 저장소로 보고, blob/tree/commit/tag 객체 구조와 commit 생성 과정을 살펴봤고

2편에서는 commit이 parent 링크로 이어지는 DAG 구조, 그 위에 올라가는 branch/ref/HEAD/tag/reflog를 정리했습니다.

 

이번 편에서는 merge, rebase, cherry-pick을 실제 예제와 다이어그램으로 정리해봤습니다.

merge는 갈라진 히스토리를 새 merge commit으로 합치는 방식, rebase는 브랜치의 base를 바꾸면서 커밋들을 새로 만드는 방식이며 마지막으로 cherry-pick은 특정 커밋만 골라 복사해오는 방식이었습니다.

 

 세 편에서 정리한 내용을 하나로 합치면, Git은 해시 기반 K-V 저장소 위에 commit DAG를 쌓고, 그 위에서 branch/HEAD/tag 같은 ref를 옮기면서 작업하는 도구 라고 최종 요약할 수 있을 것 같습니다.

 

Git을 많이 사용하기 때문에, 단순히 명령어만 익히는 것이 아니라 내부 동작을 어느 정도 이해해 보고 싶어서 이번 시리즈를 학습하고 정리해봤습니다. 조금 더 적재적소에 적절한 명령어를 사용하고 특히 오픈소스를 기여하면서 무수히 많은 개발자들의 commit, branch와 유기적으로 잘 분리하고 병합하여 기여해나갈 수 있겠다는 생각이 듭니다.

 

 

 

 

References.

https://git-scm.com/docs/git-merge

https://git-scm.com/docs/git-rebase

https://git-scm.com/docs/git-cherry-pick

https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging

https://git-scm.com/book/en/v2/Git-Branching-Rebasing

https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History

https://docs.github.com/en/get-started/using-git/about-git-rebase

https://docs.github.com/en/get-started/using-git/using-git-rebase-on-the-command-line

https://docs.github.com/articles/about-pull-request-merges

https://docs.gitlab.com/topics/git/git_rebase

https://docs.gitlab.com/user/project/merge_requests/cherry_pick_changes

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록