Git— 스냅샷으로 이해하는 버전 관리
7장

충돌 해결과 스태시

충돌은 Git의 실패가 아니라 Git의 정직함이다 — 두 히스토리가 같은 줄을 다르게 바꾼 사실을 솔직히 드러내고, 인간이 판단하도록 멈추는 것이다.

충돌이 발생하는 이유

두 브랜치가 같은 파일의 같은 영역을 서로 다르게 수정했을 때, Git은 어느 쪽을 선택해야 할지 스스로 판단할 수 없다. 그때 충돌이 발생한다.

Git의 3-way merge를 이해하면 충돌의 발생 조건이 명확해진다. Git은 merge할 때 세 개의 스냅샷을 참조한다: 공통 조상(base), 현재 브랜치(ours), 들어오는 브랜치(theirs). 공통 조상 대비 한쪽만 바꿨다면 자동으로 그쪽을 선택한다. 하지만 양쪽 모두 같은 영역을 서로 다르게 바꿨다면 Git은 어느 쪽이 "맞는" 변경인지 알 방법이 없다 — 이것이 충돌이다.

충돌이 발생하면 Git은 merge를 중단하고 충돌 마커를 파일에 기록한다. 이 상태에서 git status를 실행하면 "both modified" 또는 "unmerged" 상태의 파일 목록이 나타난다.

3-way merge — Git이 두 브랜치를 합칠 때 공통 조상을 기준으로 각 브랜치의 변경 내용을 비교하는 방식. 양쪽이 같은 영역을 수정하면 충돌로 처리된다.
직관: 충돌은 "버그"가 아니라 "질문"이다. Git이 "이 부분은 내가 결정할 수 없어 — 당신이 판단해 줘"라고 요청하는 것이다. 충돌이 잦다면 코드를 함께 수정하는 패턴을 줄이거나, 더 자주 merge/rebase하는 것이 도움이 된다.

충돌 마커 읽기

Git이 충돌 파일에 삽입하는 마커는 처음 보면 낯설지만, 구조를 이해하면 바로 읽을 수 있다.

충돌 파일에는 세 종류의 구분자가 삽입된다:

  • <<<<<<< HEAD — 현재 브랜치(ours)의 내용 시작
  • ======= — 두 버전의 경계선
  • >>>>>>> feature/login — 들어오는 브랜치(theirs)의 내용 끝

<<<>>> 사이의 전체 블록이 충돌 영역이다. 해결 방법은 이 영역을 원하는 최종 내용으로 편집하고, 마커 라인(<<<, ===, >>>)을 모두 삭제하는 것이다. 현재 브랜치 것만 선택할 수도, 들어오는 것만 선택할 수도, 둘을 섞어서 새로운 내용을 만들 수도 있다.

충돌 마커가 하나라도 남아 있으면 코드가 컴파일되지 않는다. 편집기에서 <<<<<<<를 전체 검색해서 남은 마커가 없는지 확인하는 것이 좋다.

충돌 마커 예시
def greet(name):
<<<<<<< HEAD
    return f"Hello, {name}!"
=======
    return f"안녕하세요, {name}님!"
>>>>>>> feature/i18n
충돌 해결 워크플로
git status                  # 충돌 파일 확인
# 편집기로 파일을 열어 마커 구간 수정
git add <파일>               # 해결됨으로 표시
git merge --continue        # merge 완료 (또는 git commit)
# 중단하고 싶다면:
git merge --abort           # merge 이전 상태로 완전히 복구

충돌 해결 도구와 전략

텍스트 편집기로 직접 해결하는 것이 기본이지만, 더 복잡한 충돌에는 전용 머지 도구를 사용하면 시각적으로 비교하기 쉽다.

git mergetool을 실행하면 설정된 GUI 머지 도구(vimdiff, VS Code, IntelliJ IDEA, Beyond Compare 등)가 열리며 공통 조상·현재·들어오는 버전을 나란히 보여준다. 도구 안에서 편집하고 저장하면 자동으로 git add까지 처리해 주는 도구도 있다.

경우에 따라서는 한쪽을 통째로 선택하는 것이 적절하다:

  • git checkout --ours <파일> — 현재 브랜치 버전을 통째로 선택
  • git checkout --theirs <파일> — 들어오는 브랜치 버전을 통째로 선택

그러나 이 전략은 충돌 영역을 한 줄씩 판단하지 않고 파일 전체를 바꾸므로, 상대방의 변경이 완전히 무시되는 위험이 있다. 바이너리 파일(이미지, 컴파일된 결과물 등)에서 더 자주 쓰이는 패턴이다.

핵심: 충돌 해결 후 반드시 테스트를 실행하라. 두 버전을 수동으로 합칠 때 논리적 오류가 생기기 쉽고, 컴파일 오류 없이도 런타임 버그가 생길 수 있다.

git stash — 변경사항을 잠시 치워두기

git stash는 uncommitted 변경사항(작업 트리 + 스테이징 영역)을 임시 스택에 저장하고, 작업 디렉터리를 마지막 커밋 상태로 되돌린다.

stash가 필요한 상황은 흔하다. 작업 도중 긴급 버그 수정이 들어왔을 때, 아직 커밋하기엔 이른 미완성 코드가 있을 때, 원격 변경사항을 pull 해야 하는데 uncommitted 변경이 있을 때. stash로 잠시 치워두고, 다른 작업을 처리한 뒤 git stash pop으로 되가져오면 된다.

stash는 내부적으로 특수한 커밋 두 개(스테이지 트리와 작업 트리)를 생성해 refs/stash 포인터로 관리한다. 여러 번 stash하면 스택이 쌓이며, git stash list로 전체 목록을 볼 수 있다.

중요한 제약: stash는 기본적으로 추적되지 않는 파일(untracked files)을 저장하지 않는다. -u 옵션을 추가해야 untracked 파일도 포함된다. --all은 gitignore된 파일까지 포함한다.

stash 명령
git stash                      # 변경사항을 stash 스택에 저장
git stash push -m "WIP: 로그인 폼" # 메시지와 함께 저장
git stash -u                   # untracked 파일도 포함
git stash list                 # stash 목록 조회
git stash pop                  # 최근 stash를 꺼내 적용 + 삭제
git stash apply stash@{1}      # 특정 stash 적용 (삭제하지 않음)
git stash drop stash@{0}       # 특정 stash 삭제
git stash clear                # 모든 stash 삭제
git stash show -p stash@{0}    # stash 내용 diff 보기
함정: git stash pop도 충돌이 발생할 수 있다. stash한 이후 같은 파일을 수정했다면 pop 시 충돌 마커가 생긴다. 이 경우 merge 충돌과 동일하게 해결하면 된다.

stash 고급 활용

stash는 단순한 임시 저장소 이상으로, 브랜치 전환 워크플로와 결합하면 강력한 패턴을 만들 수 있다.

stash → 새 브랜치 생성: git stash branch <브랜치명>은 stash를 저장한 시점의 커밋에서 새 브랜치를 만들고 stash를 그 브랜치에 자동으로 적용한다. "이 작업, 사실 다른 브랜치에서 해야 했는데"라는 상황에 완벽하다.

부분 stash: git stash push -p는 파일별, 헝크(hunk)별로 어느 부분을 stash할지 대화식으로 선택한다. 한 파일에 두 가지 서로 다른 작업이 섞여 있을 때 유용하다.

스테이지만 stash: git stash --staged는 스테이지된 변경만 저장하고 unstaged 변경은 작업 트리에 남긴다.

stash 고급 사용
git stash branch hotfix/urgent    # stash를 새 브랜치로
git stash push -p                  # 부분 선택 stash
git stash --staged                 # staged 변경만 stash
git stash show -p stash@{0}        # 특정 stash 내용 확인
직관: stash는 "서랍"이다. 작업 중인 것을 서랍에 넣어두고, 다른 일을 처리하고, 다시 꺼내서 이어서 작업한다. 단, 서랍 속 내용이 오래될수록 꺼낼 때 충돌 가능성이 높아진다 — 너무 오래 보관하지 말고 되도록 빨리 커밋으로 전환하는 것이 좋다.

충돌 상태와 stash 시각화

아래 애니메이션은 두 브랜치가 같은 파일을 수정한 뒤 merge할 때 충돌이 발생하는 과정, 그리고 stash의 저장과 복원을 단계별로 보여준다.

충돌 발생 → 해결 → 머지 커밋 1 / 4
SPACE 재생 · ← → 이동 · R 리셋
git stash — 변경사항 보관과 복원 1 / 3
SPACE 재생 · ← → 이동 · R 리셋
← 되돌리기태그와 릴리스 워크플로 →