Git 알고 쓰기 (2) merge, rebase, reset, revert는 언제 써야 할까
브랜치를 합치는 merge와 rebase, 커밋을 되돌리는 reset과 revert를 비교합니다.
On this page
이 글은 책 『팀 개발을 위한 Git, GitHub 시작하기』를 읽으면서 남긴 Git/GitHub 메모를 블로그용으로 다시 정리한 글입니다. 첫 번째 글에서 커밋, HEAD, 브랜치 포인터를 봤다면, 이번 글에서는 그 포인터와 커밋 이력을 실제로 움직이는 명령어들을 비교합니다.
이 글에서는 무서운 명령어 4종 세트 merge(충돌 나면 무서움), rebase, reset, revert를 다뤄본다.
처음에는 네 명령어가 다 비슷한 점이 있다.
merge: 합치는 거?rebase: 이것도 합치는 거?reset: 되돌리는 거?revert: 이것도 되돌리는 거?
먼저 크게 나누면 이렇게 볼 수 있다.
브랜치를 합치는 쪽
├── merge
└── rebase
커밋을 되돌리는 쪽
├── reset
└── revert
merge와 rebase는 두 작업 내용을 어떻게 합칠 것인가의 문제다.
reset과 revert는 이미 만든 커밋을 어떻게 되돌릴 것인가의 문제다.
이 구분만 해도 반은 왔다고 볼 수 있다. 나머지 반은 “공유된 이력을 건드리는가?”이다.
merge: 다른 브랜치를 내 몸에 붙인다
git merge OOO⇒ OOO 브랜치를 내 몸, 즉 기준 브랜치에 붙인다고 생각하면 외우기 쉽다!
merge는 현재 내가 서 있는 브랜치에 다른 브랜치의 변경사항을 합친다.
git checkout main
git merge feature/login
# feature/login 브랜치를 main에 합친다.
# 기준은 현재 브랜치인 main이다.
여기서 중요한 것은 기준 브랜치다.
main에서 git merge feature/login을 하면 feature/login의 결과를 main에 반영하겠다는 뜻이다. 반대로 feature/login에서 git merge main을 하면 main의 변경사항을 내 기능 브랜치에 먼저 가져와서 확인하겠다는 뜻이 된다.
☞ 합칠 때는 “어떤 브랜치를 기준으로 병합할 것인지”를 먼저 봐야 한다. 이걸 잘못 보면 내 몸에 붙일 걸 남의 몸에 붙이게 될 수 있다.
merge 결과는 크게 세 가지다.
- fast-forward
- merge commit
- conflict
fast-forward는 말 그대로 빨리 감기다. main이 그냥 feature/login의 최신 커밋을 가리키도록 앞으로 움직이면 끝나는 경우라서 깔끔하다.
before
A---B main
\
C---D feature/login
after
A---B---C---D main, feature/login
그냥 뒤쪽 상태로 따라가면 된다.
반대로 두 브랜치가 서로 다른 커밋을 만든 상태라면 병합 커밋이 생길 수 있다.
A---B---C main
\ \
D---M
feature/login
이때 M이 merge commit이다. 커밋 내역에 “여기서 두 작업 내용을 합쳤음”이라는 기록이 남는다. 지저분해 보일 수도 있지만 협업 맥락을 남긴다는 점에서 장점도 있다.
충돌은 동일한 부분을 서로 다르게 고쳤을 때 생긴다.
git merge feature/login
# Automatic merge failed
# 충돌난 파일을 직접 고치고 add한 뒤 commit한다.
처음 충돌을 겪으면 헉! 하며 식은땀이 날 수도 있지만, 충돌이 났다고 해서 망한 것은 아니며 귀찮아 질 순 있다. Git이 “이 부분은 내가 판단 못 하겠으니 사람이 정해줘”라고 멈춘 것이다. 충돌난 파일을 열어보면 이렇게 되어 있다.
<<<<<<< HEAD
내 변경사항
=======
상대방 변경사항
>>>>>>> feature/login
이 부분을 고쳐서 내가 원하는 최종 결과로 만들고, git add로 표시한 뒤 git commit하면 된다.
rebase: base를 갈아 끼운다
rebase는 이름 그대로 base를 다시 배치해주는 명령이다. 재배치의 원리는 아래와 같다.
- HEAD와 대상 브랜치의 공통 조상을 찾는다.
- 공통 조상 이후에 생성된 내 커밋들을 대상 브랜치 뒤로 재배치한다.
- 이때 재배치된 커밋들은 이전 커밋의 사본이므로, 이전 커밋과 같은 아이디를 갖지 않는다.
말은 길지만 그림으로 보면 이렇다.
before
A---B---C main
\
D---E feature/login
after rebase
A---B---C main
\
D'---E' feature/login
D, E가 C 뒤로 옮겨져 D', E'가 된다. 여기서 따옴표가 중요하다. 같은 변경사항처럼 보여도 커밋 아이디는 달라진다. 커밋은 부모 커밋 정보도 포함하기 때문에, 부모가 바뀌면 커밋 자체도 달라진다.
git checkout feature/login
git rebase main
# feature/login의 커밋들을 main 최신 커밋 뒤에 다시 쌓는다.
이해하기 쉽게 설명하자면!
merge OOO = OOO 브랜치를 내 몸으로 합친다.
rebase OOO = OOO 브랜치 뒤로 줄줄이 사탕마냥 내 커밋을 이어 붙인다.
그래서 rebase의 장점은 히스토리가 깔끔하다는 것이다. main 뒤에 내 작업 커밋이 일렬로 붙는다.
A---B---C---D'---E'
보기엔 예쁘지만 but 조심해야 할 점이 있다.
원격에 이미 푸시한 브랜치, 다른 사람이 같이 쓰는 브랜치는 함부로 rebase하지 않는 편이 좋다.
rebase는 커밋을 다시 만든다. 커밋 아이디가 바뀐다. 즉, 다른 사람이 알고 있던 이력과 내가 새로 만든 이력이 달라질 수 있다. 가급적 푸시하지 않은 혼자 사용하는 로컬 브랜치에만 적용하자!
rebase 중 충돌이 나면 merge와 조금 다르게 진행한다.
git rebase main
# 충돌 발생
git status
# 충돌난 파일 확인
git add 충돌난파일
git rebase --continue
# 충돌을 해결할 때마다 계속 입력한다.
※ merge 충돌은 보통 해결 후 commit한다. rebase 충돌은 해결 후 git rebase --continue를 입력한다.
merge와 rebase 비교
둘 다 브랜치의 변경사항을 합칠 때 쓰지만 결과 그래프 모양이 달라진다.
| 구분 | merge | rebase |
|---|---|---|
| 목적 | 두 브랜치 흐름을 합친다 | 내 커밋의 시작점을 새 base 뒤로 옮긴다 |
| 그래프 모양 | 분기와 병합 기록이 남을 수 있음 | 일렬로 깔끔해짐 |
| 커밋 아이디 | 기존 커밋을 그대로 둔다 | 내 커밋을 새로 만들 수 있다 |
| 협업 안정성 | 공유 브랜치에서도 비교적 안전 | 공유된 이력에서는 조심 |
| 충돌 해결 | 해결 후 add + commit | 해결 후 add + rebase --continue |
즉, 정리하자면 다음과 같다.
- 이미 공유된 브랜치라면
merge가 안전하다. - 아직 나 혼자 쓰는 기능 브랜치라면
rebase로 정리해도 된다. - PR 올리기 전에 내 브랜치를 최신
main위로 깔끔하게 올리고 싶다면rebase가 좋다.
reset: 브랜치 포인터를 과거로 돌린다
reset은 “옛날 커밋으로 브랜치를 되돌리고 싶을 때” 쓰는 명령이다.
그런데 그냥 되돌린다고 말하면 부족하다. reset은 현재 브랜치 포인터를 특정 커밋으로 옮기고, 옵션에 따라 index와 working tree까지 같이 바꾼다.
첫 번째 글에서 봤던 세 공간을 다시 가져오면 이렇다.
HEAD / current branch
index / staging area
working tree
reset은 이 세 공간을 얼마나 되돌릴지 옵션으로 나눈다.
git reset --soft HEAD~1
# 브랜치 포인터만 이전 커밋으로 이동한다.
# 변경사항은 staged 상태로 남는다.
git reset --mixed HEAD~1
# 기본값이다.
# 브랜치 포인터와 index를 이전 커밋 기준으로 맞춘다.
# 변경사항은 working tree에 남는다.
git reset --hard HEAD~1
# 브랜치 포인터, index, working tree를 모두 이전 커밋 기준으로 맞춘다.
# 작업 폴더의 변경사항도 날아갈 수 있으므로 조심한다!
soft/mixed가 원하는 커밋으로 이력을 되돌리지만, 이후 변경사항을 다시 stage에 추가할 수 있게 데리고 오는 것이라면, hard는 “변경사항 이력을 남기지 않고 말 그대로 리셋”이다.
soft -> 커밋만 취소하고 변경사항은 staged에 둠
mixed -> 커밋만 취소하고 변경사항은 working tree에 둠
hard -> 커밋도, index도, working tree도 그 시점으로 돌림
reset --hard는 나만 쓸때만 하는 것이 좋다. 원격에 이미 푸시한 브랜치를 reset --hard로 과거로 돌리는 건 위험하다. 다른 사람의 로컬 이력과 충돌할 수 있기 때문이다.
git reset --hard <commit>
git push --force-with-lease
# 이미 원격에 올린 이력을 바꾸는 경우 강제 푸시가 필요할 수 있다.
# 혼자 쓰는 브랜치인지 확인. fork, clone한 사람 없는지 확인.
강제 푸시는 “내가 이 브랜치의 이력을 다시 쓰겠다”는 것이므로 협업 중인 브랜치라면 다른 사람의 로컬 이력과 충돌할 수 있다. 혼자 쓰는 실험 브랜치에서는 상관 없지만 공유 브랜치에서는 조심해야 한다.
revert: 되돌리는 커밋을 새로 만든다
revert는 reset과 다르다.
reset은 브랜치 포인터를 과거로 옮긴다. 반면 revert는 기존 커밋을 취소하는 새 커밋을 만든다.
before
A---B---C main
after revert C
A---B---C---R main
R은 C의 변경사항을 되돌리는 커밋이다. C를 없애는 것이 아니므로 C는 히스토리에 그대로 남는다.
git revert <commit>
# 해당 커밋의 변경사항을 취소하는 새 커밋을 만든다.
핵심은 reset과 달리, 변경사항을 되돌리는 커밋을 또 하나 생성하는 것! 이력 관리가 중요한 경우 이렇게 사용한다고 한다.
이미 main에 merge된 기능을 취소해야 한다면 보통 revert가 더 안전하다. 공개된 이력을 지우지 않고 “이 기능을 다시 되돌렸다”는 기록을 남기기 때문이다.
예를 들어 이런 상황이다.
- 기능 브랜치를
main에 병합했다. - 배포 후 문제가 발견됐다.
- 기능 전체를 일단 빼야 한다.
- but 이미
main에 올라갔고 다른 사람들도 그 이력을 기준으로 작업 중이다.
이때 reset --hard로 main을 과거로 돌리는 건 위험하다. 보통은 revert로 취소 커밋을 만든다.
git revert <problem-commit>
# 또는 merge commit을 되돌릴 때는 별도 옵션이 필요할 수 있다.
※ merge commit을 revert하는 경우에는 부모가 둘 이상이라 추가 판단이 필요하다. 이 글에서는 기본 비교가 목적이라 깊게 들어가진 않지만, “merge commit revert는 그냥 일반 커밋처럼 생각하면 안 된다” 정도는 기억해두자.
reset과 revert 비교
둘 다 “되돌리기”처럼 보이지만 성격이 완전히 다르다.
| 구분 | reset | revert |
|---|---|---|
| 목적 | 브랜치 포인터를 과거 커밋으로 이동 | 기존 커밋을 취소하는 새 커밋 생성 |
| 이력 | 기존 이력을 다시 쓸 수 있음 | 기존 이력을 보존 |
| 원격 공유 후 사용 | 매우 조심!! | 비교적 안전 |
| 주 사용 상황 | 로컬 커밋 정리, 실험 브랜치 복구 | 이미 공유된 변경사항 취소 |
아래처럼 암기해두면 편할 것이다.
- 아직 내 로컬에서만 만든 커밋을 정리한다 →
reset - 이미 원격에 올렸고 다른 사람도 볼 수 있다 →
revert - 작업 폴더 변경사항까지 날려도 된다 →
reset --hard, but 확인 필수 - 기록을 남겨야 한다 →
revert
Takeaway
정리하면 이렇게 볼 수 있다.
| 명령어 | 한 줄 설명 | 이력 | 주의점 |
|---|---|---|---|
merge | 다른 브랜치를 현재 브랜치에 합친다 | 보통 기존 이력을 보존 | 기준 브랜치 선택 잘하기 |
rebase | 내 커밋을 새 base 뒤로 다시 쌓는다 | 내 커밋을 새로 만들 수 있음 | 공유 브랜치에서 조심 |
reset | 브랜치 포인터를 과거로 옮긴다 | 그럴 수 있음 | --hard, 강제 푸시 조심 |
revert | 되돌리는 새 커밋을 만든다 | 기존 이력 보존 | 대상 커밋 선택 조심 |
역시 아래처럼 외워두자!
- merge는 대상 브랜치를 내 몸에 붙이는 것.
- rebase는 base가 되는 커밋을 바꾸는 것.
- reset은 옛날 커밋으로 브랜치를 되돌리는 것.
- revert는 되돌리는 커밋을 또 하나 생성하는 것.
참고 자료
- 정호영, 진유림, 『팀 개발을 위한 Git, GitHub 시작하기』, 한빛미디어, 2020.
- Pro Git Book: Git Branching - Rebasing
- Git Documentation: git-reset
- Git Documentation: git-revert
Keep reading
Series
Git 원리 알고 쓰기
Related posts


Comments