Git merge와 rebase 차이
Git 브랜치의 변경을 합칠 때 merge와 rebase를 자주 사용한다.
두 명령 모두 한 브랜치의 작업을 다른 브랜치 위에 통합할 수 있지만, 만들어지는 커밋 그래프는 다르다.
merge는 갈라졌던 이력을 연결해서 분기 구조를 보존한다.rebase는 커밋을 새 기준점 위에 다시 적용해서 한 줄에 가까운 이력을 만든다.
어느 쪽이 더 좋은지를 먼저 정하기보다 어떤 이력을 남기고 누구와 공유한 커밋인지부터 판단해야 한다.
출발점이 되는 커밋 그래프
main과 feature가 공통 커밋 B 이후에 갈라진 상태를 보자.
C---D main
/
A---B
\
E---F featureC, D는 main에만 있고 E, F는 feature에만 있다.
두 브랜치가 가리키는 마지막 커밋은 다르지만 공통 조상은 B다.
브랜치는 커밋 자체를 복사한 별도 폴더가 아니라 특정 커밋을 가리키는 이름이다.merge와 rebase의 차이는 이 포인터와 커밋의 부모 관계를 어떻게 바꾸는지에서 드러난다.
merge가 만드는 이력
main으로 이동한 뒤 feature를 merge하면 Git은 두 브랜치의 변경을 통합한다.
git switch main
git merge feature위처럼 이미 이력이 갈라져 있다면 Git은 공통 조상 B, 현재 브랜치의 D, 합칠 브랜치의 F를 이용해 three-way merge를 수행한다.
자동으로 통합할 수 있으면 결과 snapshot을 담은 merge commit M을 만든다.
C---D
/ \
A---B M main
\ /
E---F featureM은 D와 F를 부모로 갖는다.
그래서 로그에는 두 작업이 B 이후에 병렬로 진행되었다가 M에서 합쳐졌다는 사실이 남는다.
기존의 C, D, E, F는 다시 만들지 않는다.
브랜치의 실제 분기와 통합 시점을 보존하고 싶을 때 merge가 잘 맞는다.
fast-forward의 예외
git merge가 항상 merge commit을 만드는 것은 아니다.
현재 브랜치가 합칠 브랜치의 조상이라면 이력이 갈라져 있지 않으므로 fast-forward가 가능하다.
A---B main
\
C---D feature이 상태에서 main에 feature를 merge하면 Git은 기본적으로 새 커밋을 만들 필요가 없다.main 포인터를 B에서 D로 앞으로 옮기기만 한다.
A---B---C---D main, feature이 동작도 Git에서는 merge의 한 결과지만 별도의 merge commit은 없다.
기능 브랜치가 합쳐진 경계를 커밋으로 남기고 싶다면 --no-ff를 사용한다.
git switch main
git merge --no-ff feature반대로 fast-forward일 때만 통합하고, 분기된 상태라면 명령을 실패시키고 싶을 때는 --ff-only를 사용한다.
git merge --ff-only feature따라서 로그에서 merge commit이 보이는지는 merge 명령을 사용했는지만으로 판단할 수 없다.
통합 직전의 그래프와 --ff, --no-ff, --ff-only 정책을 함께 봐야 한다.
rebase가 만드는 이력
같은 출발점에서 feature로 이동해 main을 기준으로 rebase해 보자.
git switch feature
git rebase mainGit은 feature가 공통 조상 B 이후에 만든 변경을 찾는다.
그다음 main의 끝인 D를 새 기준으로 삼고 E, F의 변경을 순서대로 다시 적용한다.
A---B---C---D main
\
E'---F' feature여기서 E', F'는 원래 E, F가 자리를 옮긴 것이 아니다.
같은 변경을 새 부모 위에 적용해 만든 새로운 커밋 객체다.
커밋 ID는 파일 snapshot만으로 정해지지 않고 부모 커밋 같은 정보에도 영향을 받는다.
부모가 바뀐 E', F'는 원래 커밋과 다른 ID를 갖는다.
그래서 rebase를 이력 재작성이라고 부른다.
rebase가 끝난 뒤 main에서 feature를 merge하면 fast-forward할 수 있다.
git switch main
git merge --ff-only feature최종 그래프는 한 줄로 이어진다.
A---B---C---D---E'---F' main, feature실제로는 두 작업이 병렬로 진행되었지만 최종 이력은 main 작업 뒤에 feature 작업을 순서대로 한 것처럼 보인다.
이 단순한 로그가 rebase의 장점인 동시에 실제 분기 구조를 남기지 않는다는 특성이다.
결과 snapshot과 이력의 차이
충돌을 같은 방식으로 해결했다면 merge의 마지막 M과 rebase 뒤의 마지막 F'가 가리키는 파일 snapshot은 같을 수 있다.
달라지는 핵심은 최종 파일보다 그 결과에 도달한 부모 관계와 커밋 ID다.
merge는 기존 커밋을 유지하고 필요할 때 부모가 둘 이상인 커밋으로 이력을 연결한다.
rebase는 대상 커밋의 변경을 다른 기준점 위에 재적용해 새로운 커밋 열을 만든다.
이 차이는 git log --graph로 확인하기 쉽다.
git log --graph --oneline --decorate --all분기와 통합 시점이 중요한 저장소에서는 merge 그래프가 유용한 정보가 된다.
각 기능의 커밋을 한 방향으로 읽는 것이 중요한 저장소에서는 rebase된 그래프가 단순하다.
merge 충돌 처리
두 브랜치가 같은 파일의 같은 부분을 다르게 수정하면 Git이 결과를 자동으로 정하지 못할 수 있다.
merge는 이때 중단되고 충돌 파일과 index를 해결 대기 상태로 남긴다.
git status파일의 충돌 표시를 확인하고 의도한 최종 내용으로 수정한 뒤 stage에 올린다.
git add path/to/conflicted-file
git merge --continue모든 충돌을 해결하고 stage에 올리면 merge commit을 완성할 수 있다.
진행 중인 merge를 취소하고 시작 전 상태로 돌아가려면 다음 명령을 사용한다.
git merge --abortmerge 전에 작업 트리를 깨끗하게 만드는 편이 안전하다.
커밋하지 않은 변경이 많으면 충돌 상태와 원래 작업을 구분하기 어려워지고, 상황에 따라 --abort가 시작 전 변경을 완전히 복구하지 못할 수도 있다.
rebase 충돌 처리
rebase도 충돌을 자동으로 없애는 명령은 아니다.
Git은 원래 커밋을 하나씩 새 기준점 위에 재적용하다가 충돌한 커밋에서 멈춘다.
충돌을 수정하고 stage에 올린 뒤 rebase를 계속한다.
git add path/to/conflicted-file
git rebase --continue여러 커밋이 같은 부분을 단계적으로 수정했다면 rebase 중 충돌을 여러 번 해결할 수 있다.
각 단계에서는 현재 다시 적용 중인 커밋의 의도를 확인해야 한다.
전체 rebase를 취소하려면 다음 명령을 사용한다.
git rebase --abortgit rebase --skip으로 현재 커밋을 건너뛸 수도 있지만, 그 커밋의 변경이 최종 이력에서 빠진다.
단순히 충돌이 번거롭다는 이유로 사용하지 말고 해당 변경이 이미 반영되었거나 정말 불필요한지 확인해야 한다.
공유 브랜치의 안전 원칙
안전 기준은 단순하다.
다른 사람이 내려받아 그 위에서 작업했을 수 있는 커밋은 rebase하지 않는다.
공유된 커밋을 rebase하면 기존 커밋 대신 ID가 다른 새 커밋이 생긴다.
동료의 브랜치는 여전히 예전 커밋을 부모로 삼고 있으므로 같은 변경이 중복되어 보이거나 불필요한 충돌과 merge가 생길 수 있다.
main, release, 여러 명이 함께 쓰는 feature 브랜치처럼 공용 기준이 되는 브랜치에서는 이력을 보존하는 merge가 안전한 기본값이다.
이미 원격에 push한 개인 PR 브랜치도 누군가 그 위에 작업하고 있다면 공유 브랜치로 취급한다.
rebase는 아직 push하지 않은 개인 작업이나, 다른 사람이 기반으로 삼지 않는다고 팀에서 명확히 합의한 브랜치에 사용하는 편이 안전하다.
원격 개인 브랜치의 rebase를 허용하는 팀이라도 강제 push 전에는 리뷰 도구와 협업자의 상태를 확인해야 한다.
git push --force-with-lease는 원격이 예상과 달라졌을 때 덮어쓰기를 거부해 주지만, 공유 이력을 재작성해도 된다는 허가를 만들어 주지는 않는다.
보호 브랜치의 규칙과 팀의 통합 정책이 명령의 편의보다 우선한다.
merge 중심 작업 흐름
분기 구조와 기능 단위의 통합 시점을 남기려면 feature 브랜치를 main에 merge한다.
git fetch origin
git switch main
git merge --ff-only origin/main
git merge --no-ff feature
git push origin main첫 번째 --ff-only merge는 로컬 main이 원격보다 갈라졌다면 중단되어 실수로 별도 merge commit을 만드는 일을 막는다.
두 번째 --no-ff merge는 feature 경계를 명시적인 merge commit으로 남긴다.
실제 저장소에서는 직접 main에 push하기보다 pull request와 보호 브랜치를 통해 같은 정책을 적용하는 경우가 많다.
중요한 점은 팀이 fast-forward, merge commit, squash 중 어떤 통합 방식을 쓰는지 일관되게 정하는 것이다.
rebase 중심 개인 작업 흐름
공유 전의 개인 feature 브랜치를 최신 main 위로 정리하려면 원격을 fetch한 뒤 rebase한다.
git fetch origin
git switch feature
git rebase origin/main충돌이 나면 파일을 수정하고 git add와 git rebase --continue를 반복한다.
완료한 뒤 테스트를 실행하고 커밋 그래프와 변경 내용을 다시 확인한다.
git log --graph --oneline --decorate --all
git diff origin/main...feature아직 원격에 올리지 않은 브랜치라면 일반 push로 게시할 수 있다.
git push -u origin feature이미 게시한 브랜치라면 무조건 강제 push하기 전에 그 브랜치가 실제로 개인 소유인지와 팀이 이력 재작성을 허용하는지 확인해야 한다.
공유 여부를 확신할 수 없다면 merge로 최신 기준 브랜치를 받아들이는 편이 안전하다.
선택 기준
실제 분기와 통합 기록을 보존해야 하면 merge를 선택한다.
여러 사람이 기준으로 삼는 브랜치이거나 merge commit 자체가 리뷰와 배포의 경계라면 특히 잘 맞는다.
아직 공유하지 않은 개인 커밋을 최신 기준 위에 정리하고 선형 로그를 만들고 싶다면 rebase를 선택한다.
작은 커밋을 제출 전에 정돈하는 작업에도 rebase가 유용하지만, 커밋 ID가 바뀐다는 사실을 전제로 해야 한다.
충돌이 생기는지 여부만으로 둘을 고르기는 어렵다.
두 방식 모두 같은 변경 지점에서 충돌할 수 있고, rebase는 커밋별로 충돌이 반복될 수도 있다.
팀에서는 개인 브랜치를 공유 전에 rebase하고 공용 브랜치에는 merge하는 혼합 흐름도 많이 사용한다.
핵심은 개인 이력을 정리하는 단계와 공유 이력을 통합하는 단계를 구분하는 것이다.
정리
merge는 기존 커밋을 유지하고 갈라진 이력을 연결한다.
다만 fast-forward가 가능하면 기본적으로 새 merge commit 없이 브랜치 포인터만 이동할 수 있다.
rebase는 커밋의 변경을 새 기준 위에 다시 적용한다.
부모 관계가 달라지므로 새로운 커밋과 새로운 ID가 만들어지고, 결과 이력은 한 줄에 가까워진다.
개인적이고 아직 공유하지 않은 커밋은 rebase로 정리할 수 있다.
다른 사람이 기반으로 삼을 수 있는 공유 커밋은 재작성하지 않고 merge로 통합하는 것이 안전한 기준이다.
참고 자료


















