Git 정리 - 기본 개념부터 Merge, Rebase, Cherry-pick, 충돌 해결까지

#Study
Git은 거의 모든 개발 환경에서 쓰이지만, 막상 merge 나 rebase, conflict 해결까지 익숙한 경우는 많지 않다.
개발을 하다 보면 add - commit - push 를 넘어서, "어떻게 합칠 것인가"와 "충돌이 났을 때 내 코드를 어떻게 지킬 것인가"가 더 중요해지는 순간이 온다.
이 글에서는 Git의 공간 개념부터 Rebase, Cherry-pick, 충돌 해결까지 꼭 알아야 할 흐름을 정리해보자.

Git의 기본 흐름과 병합 이해

Git의 3가지 공간

Git을 제대로 이해하려면, 명령어보다 먼저 코드가 이동하는 3가지 공간을 알아야 한다.

Git의 3가지 공간과 흐름

공간역할관련 명령어
Working Directory현재 코드를 수정하는 작업 공간파일 편집
Staging Area커밋할 변경사항을 골라 담는 공간git add
Repository커밋이 저장되는 로컬 저장소git commit

여기에 하나를 더 붙이면 git push 로 Remote Repository 에 반영하는 흐름이 된다.

# 파일 수정 (Working Directory)
vim app.js

# 장바구니에 담기 (Staging Area)
git add app.js

# 로컬 저장소에 커밋 생성 (Repository)
git commit -m "feat: add login feature"

# 원격 저장소에 전송
git push origin main

그렇다면 왜 Staging Area 같은 중간 단계가 있는 것일까 하는 의문이 들 수 있다.
수정한 파일이 10개라고 해서 항상 10개를 한 번에 커밋해야 하는 것은 아니다. 그 중 2개만 먼저 커밋하고 싶을 때, Staging Area가 커밋 단위를 직접 제어할 수 있게 해준다.
그리고 작업하던 변경을 잠깐 치워두고 싶을 때는 git stash 도 자주 사용한다. 예를 들어 merge 나 rebase 전에 Working Directory 를 잠시 비워야 할 때 유용하다.

git add . 은 편리하지만, 의도하지 않은 파일까지 함께 스테이징될 수 있다.
가능하면 git add 파일명 또는 git add -p 로 필요한 변경만 선택하는 습관이 좋다.

브랜치를 합치는 방식 - Fast-Forward 와 3-Way Merge

브랜치를 merge 할 때 Git은 항상 같은 방식으로 합치지 않는다.
상황에 따라서 Fast-Forward 로 끝날 수도 있고, 새로운 병합 커밋(Merge Commit) 이 생길 수도 있다.

Fast-Forward

main 에서 feature 브랜치를 딴 뒤, main 에 새로운 커밋이 하나도 없다면 Fast-Forward 가 가능하다.

이 경우 Git은 별도의 병합 커밋을 만들지 않는다.

단순히 main 브랜치 포인터를 feature 의 최신 커밋으로 앞으로 이동시키면 되기 때문이다.

Fast-Forward Merge

git switch main
git merge feature

결과적으로 히스토리는 일직선처럼 남는다.

3-Way Merge

보통은 이 경우를 더 자주 보게 된다.
내가 feature 브랜치에서 작업하는 동안 다른 사람이 main 에 커밋을 추가했다면, 이제 두 브랜치는 서로 다른 방향으로 진화한 상태가 된다.
이때는 Fast-Forward 가 불가능해서 Git이 새로운 병합 커밋을 생성한다.

3-Way Merge

그래서 3-Way Merge 라고 부르는데, 비교 기준이 아래 3개이기 때문이다.

  1. 두 브랜치가 갈라지기 전 마지막 공통 조상 커밋
  2. main 의 최신 커밋
  3. feature 의 최신 커밋

이 세 지점을 비교해서 자동으로 합칠 수 있으면 merge 되고, 자동으로 합칠 수 없는 부분이 바로 충돌(conflict) 이 된다.

Fast-Forward 가 가능한 상황이어도, 기능 단위의 병합 기록을 명확히 남기고 싶다면 git merge --no-ff 브랜치명 형태를 사용할 수 있다.
팀에 따라 PR merge 시 --no-ff 를 기본으로 두기도 한다.

히스토리 조작과 충돌 해결

Cherry-pick - 원하는 커밋만 가져오기

다른 브랜치의 모든 변경이 아니라, 특정 커밋 하나만 내 브랜치로 가져오고 싶을 때 cherry-pick 을 사용한다.
대표적인 예시는 이런 경우다.

  • develop 에서 만든 핫픽스 커밋 하나만 release 에 반영해야 할 때
  • 여러 기능 커밋 중 일부 수정만 운영 브랜치에 가져와야 할 때

Cherry-pick

# 특정 커밋 하나 가져오기
git cherry-pick <커밋해시>

# 여러 개 가져오기
git cherry-pick <해시1> <해시2>

# 범위로 가져오기
# 시작해시는 끝해시보다 더 과거의 커밋이어야 한다.
git cherry-pick <시작해시>^..<끝해시>
Cherry-pick 은 커밋을 이동시키는 것이 아니라 복사하는 것이다.
원본 커밋은 기존 브랜치에 그대로 남고, 현재 브랜치에는 같은 변경을 가진 새 커밋이 생성된다.

Q. Cherry-pick 중 충돌이 났다면?

A. 작업은 일시 정지되고, 충돌 파일을 수정한 뒤 이어서 진행하면 된다.

git cherry-pick <커밋해시>

# 충돌 발생 시 상태 확인
git status

# 파일 수정 후 스테이징
git add <충돌해결파일>

# 계속 진행
git cherry-pick --continue

어떻게 해도 정리가 안 되면 아래 명령으로 시도 전 상태로 돌아갈 수 있다.

git cherry-pick --abort

Rebase 는 왜 하는 걸까?

merge 가 두 갈래의 브랜치를 하나로 합치는 작업이라면, rebase 는 내 브랜치의 시작점(base)을 다른 커밋 위로 옮긴 뒤 내 커밋들을 다시 얹는 작업이다.

보통 이런 상황에서 많이 사용한다.

  • feature 브랜치에서 며칠째 작업 중이다.
  • 그 사이 main 에 다른 사람들의 커밋이 여러 개 추가되었다.
  • 내 작업을 최신 main 위 기준으로 다시 정리하고 싶다.
git switch feature
git rebase main

이 명령은 "내 브랜치 커밋들을 최신 main 뒤로 다시 배치해라"라는 의미다.

Rebase 전후 비교

그래서 히스토리가 더 일직선처럼 보이게 된다.

항목MergeRebase
히스토리분기 흔적이 남음일직선처럼 정리됨
병합 커밋생길 수 있음보통 생기지 않음
커밋 해시유지변경됨
이미 공유한 브랜치비교적 안전주의 필요
이미 원격에 push 해서 다른 사람과 공유한 브랜치를 rebase 하면, 이후에 force push 가 필요할 수 있다.
공유 브랜치라면 팀 규칙을 먼저 확인하는 편이 안전하고, 단순 git push -f 보다 git push --force-with-lease 가 훨씬 안전하다.
이 방식은 내가 모르는 사이 원격에 추가된 다른 사람의 커밋을 덮어쓰는 일을 줄여준다.
작업 브랜치에 WIP 커밋이나 자잘한 수정 커밋이 많이 쌓였다면, PR 전에 git rebase -i 로 커밋을 정리할 수 있다.
보통 pick 을 squash 나 fixup 으로 바꿔 여러 커밋을 하나로 합치면 히스토리를 더 읽기 쉽게 만들 수 있다.

충돌 해결 - Merge 와 Rebase 의 차이

충돌이 나면 파일 안에 보통 아래와 같은 표시가 생긴다.

<<<<<<< HEAD
현재 기준 코드
=======
들어오는 코드
>>>>>>>

여기서 가장 많이 헷갈리는 부분이 있는데, 바로 Merge 와 Rebase 는 충돌을 바라보는 기준이 다르다 는 점이다.

Merge vs Rebase 충돌 기준 비교

Merge 충돌일 때

예를 들어 현재 feature 브랜치에 있는 상태에서 git merge main 을 했다면, 보통 아래처럼 이해하면 된다.

구분가리키는 코드
HEAD / Current Change현재 체크아웃한 feature 브랜치의 코드
Incoming Change가져오려는 main 브랜치의 코드

즉, merge 에서는 보통 HEAD 를 "내 현재 브랜치 코드"로 이해해도 된다.
하지만 이 값이 항상 고정되는 것은 아니다.
예를 들어 main 브랜치에서 git merge feature 를 했다면, 그때는 HEAD 가 main 쪽 코드가 된다.
즉, 어느 브랜치에서 merge 를 실행했느냐에 따라 HEAD 와 Incoming 의 방향이 달라진다.

Rebase 충돌일 때

반면 feature 브랜치에서 git rebase main 을 하고 충돌이 난다면 이야기가 달라진다.

Rebase 는 먼저 기준점을 main 으로 옮긴 뒤, 내 커밋을 하나씩 다시 적용하는 과정이기 때문이다.

구분가리키는 코드
현재 기준 코드최신 main 브랜치 쪽 코드
다시 적용 중인 코드내 브랜치의 개별 커밋

즉, Rebase 충돌에서는 "현재 기준은 main 이고, 내 커밋을 그 위에 다시 얹는 중"이라고 이해하는 편이 안전하다.

에디터에 따라 Current Change, Incoming Change, ours, theirs 라벨이 다르게 보일 수 있다.
라벨 이름만 믿기보다, 지금 내가 merge 중인지 rebase 중인지 먼저 확인해야 한다.

Rebase 중 충돌 해결 흐름

Rebase 중 conflict 가 났을 때의 흐름을 명령어 기준으로 정리하면 아래와 같다.

# 1. feature 브랜치에서 최신 main 위로 재배치
git rebase main

# 2. 충돌 발생
git status

# 3. 충돌 파일 수정
# <<<<<<< HEAD
# main 쪽 코드
# =======
# 내 커밋 코드
# >>>>>>> commit-hash

# 4. 수정 후 스테이징
git add <충돌해결파일>

# 5. rebase 계속 진행
git rebase --continue

내 커밋이 여러 개라면 이 과정이 여러 번 반복될 수 있다.
여기서 중요한 점은, 충돌을 해결했다고 해서 git commit 을 직접 하는 것이 아니라는 점이다.
Rebase 는 Git이 멈춰둔 작업을 다시 이어가는 흐름이므로 git rebase --continue 를 사용해야 한다.
중간에 포기하고 싶다면 언제든 아래 명령으로 돌아갈 수 있다.

git rebase --abort

정리
✔ Git은 Working Directory, Staging Area, Repository 흐름을 먼저 이해하면 훨씬 수월해진다.
✔ merge 는 브랜치를 합치는 방법이고, 상황에 따라 Fast-Forward 또는 3-Way Merge 로 진행된다.
✔ cherry-pick 은 필요한 커밋만 골라 가져올 때, rebase 는 커밋 흐름을 다시 정리할 때 유용하다.
✔ 충돌이 났을 때는 무조건 외우기보다, 지금 어떤 기준으로 비교 중인지 먼저 파악하는 것이 중요하다.

📚 Reference