Step 88. 브랜치와 merge — 평행우주와 합치기

Step 88. 브랜치와 merge — 평행우주와 합치기

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 86의 Git 기초(add/commit/log/status), Step 87의 원격 저장소 개념을 안다.

  • 준비물: Git이 설치된 컴퓨터와 Git Bash. 모든 실험은 새 연습용 저장소에서 진행합니다.
  • 주의: 오늘은 충돌(conflict)을 일부러 일으키는 실험이 있습니다. 오류가 아니라 커리큘럼입니다. 당황하지 마세요.

잘 돌아가던 스캐너에 새 기능을 붙이다가, 반쯤 만든 상태에서 "지금 버전으로 시연해 주세요"라는 요청을 받았다고 합시다. 되돌리자니 지금까지의 작업이 아깝고, 두자니 원본이 망가져 있습니다. Git의 답은 우주를 둘로 나누는 것입니다. 브랜치(branch)는 원본을 건드리지 않고 실험하는 평행우주이고, 잘 되면 merge로 본 우주에 합칩니다. 오늘은 이 평행우주를 만들고, 합치고, 두 우주가 같은 줄을 다르게 고쳐 Git이 판단을 포기하는 순간 — 충돌 — 을 직접 해결해 봅니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 브랜치와 HEAD의 실체가 "커밋을 가리키는 화살표"임을 설명한다
  • git switch -c로 브랜치를 만들고 오가며, 각 우주의 파일 상태가 달라지는 것을 확인한다
  • fast-forward merge와 merge 커밋의 차이를 구분한다
  • 충돌 마커(<<<<<<< ======= >>>>>>>)를 읽고, 지우고, 통합안을 써서 해결한다
  • git log --graph --oneline으로 역사의 갈라짐과 합쳐짐을 읽는다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 Git Bash(Windows), Git 2.x
오늘의 명령 git branch, git switch, git merge, git log --graph --oneline, git stash, git merge --abort
필요한 개념 브랜치(포인터), HEAD, fast-forward, merge 커밋, 충돌(conflict)과 충돌 마커
오늘의 산출물 충돌 해결까지 경험한 연습 저장소 branch-lab

2-1. 브랜치의 실체 — 가리키는 화살표

브랜치라고 하면 거대한 복사본을 상상하기 쉽지만, 실체는 놀랍도록 가볍습니다. 브랜치는 "어느 커밋을 가리키는가"를 적은 포인터(화살표) 하나일 뿐입니다.

main     → 커밋C
feature  → 커밋C   (같은 곳을 가리키는 또 다른 화살표)

feature 우주에서 커밋하면 그 화살표만 앞으로 나아갑니다.

main     → 커밋C
feature  → 커밋C → 커밋D → 커밋E

파일 복사가 없으니 브랜치 만들기는 순식간입니다. 그리고 지금 어느 우주에 서 있는지를 나타내는 표시가 HEAD입니다.

2-2. merge — 두 우주를 하나로

merge는 "한 브랜치의 역사를 다른 브랜치에 합치는 것"입니다. 두 가지 대표 상황이 있습니다.

  • 빨리 감기(fast-forward): main이 가만히 있고 상대 브랜치만 앞으로 간 경우. main 화살표를 상대까지 쭉 밀기만 하면 끝. 충돌이 있을 수 없습니다.
  • 합치기 커밋(merge commit): 양쪽 다 각자 앞으로 간 경우. Git이 두 역사를 비교해 하나의 새 커밋으로 엮습니다.

2-3. 충돌 — Git이 "모르겠다"고 하는 순간

두 우주가 같은 파일의 같은 줄을 다르게 고쳤다면? Git은 어느 쪽이 맞는지 알 수 없어 판단을 인간에게 넘깁니다. 이것이 충돌(conflict)입니다. Git은 해당 파일에 이렇게 표시를 새겨 둡니다.

<<<<<<< HEAD
현재 우주의 내용
=======
상대 우주의 내용
>>>>>>> 상대브랜치

인간이 할 일은 단순합니다 — 이 표시 3종을 전부 지우고, 최종 내용을 직접 정해 쓴 뒤, add하고 commit하는 것. 충돌 해결 = 표시 3종을 지우고 정답을 쓰는 것, 그 이상도 이하도 아닙니다.


3. 따라 하기

3-1. 실험 저장소 준비

입력:

mkdir branch-lab && cd branch-lab && git init
echo "우주의 원본 파일" > story.txt
git add story.txt && git commit -m "원본: story.txt"

출력 (2026-09-09 실측):

[main (root-commit) fe80361] 원본: story.txt
 1 file changed, 1 insertion(+)

읽는 법: 커밋이 하나 있는 단 하나의 우주(main)가 생겼습니다. 모든 평행우주는 여기서 갈라집니다. 브랜치는 "갈라지는 지점"(공통 조상 커밋)이 있어야 의미가 있습니다.

3-2. 브랜치 만들고 이동하기

입력:

git switch -c feature
git branch

출력 (2026-09-09 실측):

Switched to a new branch 'feature'
* feature
  main

읽는 법: switch -c는 "만들고(create) 이동하라"는 한 번에 명령입니다. git branch의 별표가 현재 위치입니다. 앞으로의 작업은 feature 우주에 기록되고, main은 얼어붙은 채 안전하게 보존됩니다.

3-3. 평행우주에서 작업하고, 돌아와 확인하기

입력:

echo "새 기능 실험 중" >> story.txt
git add story.txt && git commit -m "실험: 새 기능 문장 추가"
git switch main
cat story.txt

출력 (2026-09-09 실측):

Switched to branch 'main'
우주의 원본 파일

읽는 법: 방금 추가한 문장이 없습니다. 놀라지 마세요 — 그 문장은 feature 우주에 안전히 있습니다. main으로 돌아오니 main의 마지막 모습이 보이는 것입니다. 다시 git switch featurecat 하면 (2026-09-09 실측) 문장이 돌아 있습니다. 폴더를 복사한 것도, 파일을 숨긴 것도 아니고 — 어느 커밋을 바라보는가만 바뀌었을 뿐입니다.

3-4. 깔끔한 합치기 — fast-forward 체험

입력 (main에 있는 상태에서):

git merge feature
cat story.txt
git log --oneline

출력 (2026-09-09 실측):

Updating fe80361..af62581
Fast-forward
 story.txt | 1 +
af62581 실험: 새 기능 문장 추가
fe80361 원본: story.txt

읽는 법: Fast-forward — main 화살표가 feature가 있던 곳까지 앞으로 밀렸을 뿐입니다. main에 새 커밋이 "생긴" 것이 아니라 feature의 커밋이 main의 것이 되었습니다. story.txt에는 실험 문장이 보입니다. 한 우주만 일한 합치기는 항상 이렇게 조용합니다.

3-5. 예측해 보기 — 양쪽이 같은 줄을 고치면?

이제 충돌을 일부러 만듭니다. 예측부터: main과 다른 브랜치 양쪽에서 story.txt의 첫 줄을 서로 다르게 고치고 merge하면?

  • (a) 나중에 커밋한 쪽이 이긴다
  • (b) 둘 다 들어간다
  • (c) Git이 멈추고 인간에게 선택을 맡긴다

직접 확인: main에서 첫 줄을 고쳐 커밋한 뒤, 갈라지는 지점(af62581)에서 새 브랜치를 만들어 거기서도 첫 줄을 다르게 고칩니다.

printf "main 우주의 첫 줄\n새 기능 실험 중\n" > story.txt
git add story.txt && git commit -m "main: 첫 줄 수정"

git switch -c rival af62581
printf "feature 우주의 첫 줄\n새 기능 실험 중\n" > story.txt
git add story.txt && git commit -m "rival: 첫 줄 수정"

git switch main
git merge rival

(Windows 메모장으로 첫 줄을 직접 고쳐도 됩니다. 해시 af62581은 여러분의 log --oneline에서 읽은 값을 쓰세요.)

출력 (2026-09-09 실측):

Auto-merging story.txt
CONFLICT (content): Merge conflict in story.txt
Automatic merge failed; fix conflicts and then commit the result.

정답은 (c) 입니다. Git은 CONFLICT를 선언하고 멈췄습니다. 고장이 아니라 "당신의 판단이 필요하다"는 요청입니다.

참고: 브랜치를 갈라지는 지점이 아니라 main의 최신 커밋에서 만들면 충돌이 아니라 fast-forward가 됩니다 (처음 실측할 때 실제로 그렇게 되어 다시 했습니다). 충돌을 보려면 양쪽이 공통 조상에서 각자 앞으로 나아가야 합니다.

3-6. 충돌 해결 — 표시를 지우고 정답을 쓰기

입력: story.txt를 편집기로 엽니다.

출력 (2026-09-09 실측, 파일 내용):

<<<<<<< HEAD
main 우주의 첫 줄
=======
feature 우주의 첫 줄
>>>>>>> rival
새 기능 실험 중

읽는 법: <<<<<<<======= 사이가 현재 우주(main), =======>>>>>>> 사이가 상대 우주(rival)입니다. git status는 이 상태를 이렇게 보여 줍니다 (2026-09-09 실측):

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
	both modified:   story.txt

할 일: 표시 3줄(<<<<<<<, =======, >>>>>>>)을 지우고 최종 내용만 남깁니다. 예:

main과 feature가 합쳐진 첫 줄
새 기능 실험 중

그리고 마무리:

git add story.txt
git commit -m "merge: rival과의 충돌 해결 — 첫 줄 통합"

출력 (2026-09-09 실측): [main 444eea2] merge: rival과의 충돌 해결 — 첫 줄 통합

왜 하는가: 충돌 해결의 전 과정이 방금 3단계였습니다 — ① 파일을 연다 ② 표시를 지우고 정답을 쓴다 ③ add/commit한다. 어떤 거대한 프로젝트의 무서운 충돌도 결국 이 3단계의 반복입니다.

⚠️ 초보 최다 사고: 표시 3종을 안 지우고 커밋해 버리는 것. <<<<<<<가 그대로 들어간 코드는 실행조차 안 됩니다. 커밋 전에 파일에서 <<<를 검색하는 습관을 들이세요.

3-7. 역사를 그림으로 보기 — log –graph

입력:

git log --graph --oneline

출력 (2026-09-09 실측):

*   444eea2 merge: rival과의 충돌 해결 — 첫 줄 통합
|\
| * 068c6cb rival: 첫 줄 수정
* | cb31f7f main: 첫 줄 수정
|/
* af62581 실험: 새 기능 문장 추가
* fe80361 원본: story.txt

읽는 법: *가 커밋, 선들이 우주의 갈라짐과 합쳐짐입니다. V자로 갈라졌다가 Λ자로 합쳐지는 모양 — 방금 여러분이 손으로 만든 역사의 지도입니다. 두 개의 부모를 가진 맨 위 커밋이 merge 커밋입니다.

3-8. 충돌이 안 나는 merge — Git의 영리함

같은 파일이라도 다른 부분을 고쳤으면 어떻게 될까요? main에서 첫 줄을, 새 브랜치 diverge에서 파일 끝에 줄을 추가한 뒤 merge해 봅니다.

출력 (2026-09-09 실측):

Auto-merging story.txt
Merge made by the 'ort' strategy.
 story.txt | 1 +
main이 다시 바꾼 첫 줄
새 기능 실험 중
꼬리 줄 추가
diverge의 마지막 줄

읽는 법: 양쪽 수정이 모두 살아 있는 채로 조용히 합쳐졌습니다. 충돌은 "같은 파일"이 아니라 "같은 줄(근처)"을 고쳤을 때만 납니다. Merge made by the 'ort' strategy는 Git이 스스로 합치기에 성공해 merge 커밋을 만들었다는 뜻입니다.

3-9. branch와 switch, 분리해서 이해하기

지금까지 git switch -c 한 줄로 만들고 이동했지만, 사실 이 명령은 두 동작의 합성입니다.

입력:

git branch idea        # 만들기만 (이동은 안 함)
git branch             # 목록 보기
git switch idea        # 이동만

읽는 법: git branch 이름은 "이 자리에 화살표 하나 더 꽂기", git switch 이름은 "HEAD를 그 화살표로 옮기기"입니다. 백업용 표시만 남기고 싶을 때는 switch 없이 만들기만 하면 됩니다.

해보기 (2026-09-09 실측): git branch -d ideaDeleted branch idea (was 444eea2). 지워지는 것은 화살표뿐이고, 그 커밋이 다른 브랜치로 이어져 있다면 역사는 그대로 남습니다.


4. 미션과 연습문제

미션 — 브랜치 전 과정 재현

  1. 새 저장소에 tool.py를 만들고 main에 기본 기능을 커밋합니다
  2. add-timeout 브랜치에서 타임아웃 기능을 추가·커밋하고 main으로 merge합니다 (Fast-forward 확인)
  3. change-msg 브랜치를 공통 조상에서 만들어, 양쪽에서 같은 줄(출력 문구)을 다르게 수정합니다
  4. merge해 CONFLICT를 발생시키고, 표시 3종을 지우고 통합안을 써서 add/commit으로 해결합니다
  5. git log --graph --oneline 결과를 노트에 옮기고, 갈라졌다 합쳐진 지점을 글로 설명합니다

연습문제

문제 1. 브랜치가 "폴더 복사"가 아니라 "화살표"라는 말을, 브랜치 생성이 순식간에 끝나는 이유와 함께 설명해 보세요.

문제 2. fast-forward merge와 merge 커밋이 각각 어떤 상황에서 일어나는지, git log --oneline 결과가 어떻게 다르게 보이는지 설명해 보세요.

문제 3. 충돌 마커 <<<<<<< HEAD, =======, >>>>>>> rival의 각 부분이 무엇을 가리키는지 설명하고, 해결 절차 3단계를 말해 보세요.

문제 4. 양쪽 브랜치가 같은 파일을 고쳤는데 충돌 없이 합쳐지는 경우는 언제이며, 그때 Git 출력에는 어떤 문구가 뜨나요?


5. 모범 답안과 완료 기준

미션 모범 답안

3번 섹션의 실측 흐름 그대로입니다. 핵심만 모으면:

git init && echo "print('v1')" > tool.py
git add tool.py && git commit -m "기본 기능"

git switch -c add-timeout
echo "timeout = 5" >> tool.py
git add tool.py && git commit -m "타임아웃 추가"
git switch main && git merge add-timeout      # Fast-forward 확인

BASE=$(git rev-parse HEAD)                    # 공통 조상 기억
echo 'MSG = "main 버전"' >> tool.py && git add tool.py && git commit -m "main: 문구"
git switch -c change-msg $BASE
echo 'MSG = "feature 버전"' >> tool.py && git add tool.py && git commit -m "change-msg: 문구"
git switch main && git merge change-msg       # CONFLICT 발생
# → tool.py를 열어 <<< === >>> 3줄을 지우고 최종 문구 작성
git add tool.py && git commit -m "merge: 문구 통합"
git log --graph --oneline

검증하는 법: ① 첫 merge 출력에 Fast-forward가 있었는가. ② 두 번째 merge에서 CONFLICT (content)가 떴는가. ③ 해결 후 tool.py에 <<<가 남아 있지 않은가(grep "<<" tool.py로 확인). ④ log --graph에 V자/Λ자 모양이 보이는가. 전부 ‘예’이면 완성입니다.

연습문제 해답

문제 1 해답. 브랜치는 커밋을 가리키는 포인터(화살표) 하나일 뿐이라, 만들 때 파일을 복사할 필요가 없기 때문에 순식간에 끝납니다. 브랜치를 오갈 때 파일이 바뀌는 것은 "어느 커밋을 바라보는가"가 바뀌기 때문입니다 (3-3 실측).

문제 2 해답. 한쪽 브랜치만 앞서 있으면 fast-forward로 화살표만 앞으로 밀리고, 양쪽이 각자 커밋했다면 두 역사를 엮는 merge 커밋이 새로 생깁니다. log에서 전자는 직선으로, 후자는 --graph에서 V자/Λ자 모양으로 보입니다 (3-4, 3-7 실측).

문제 3 해답. <<<<<<< HEAD부터 =======까지가 현재 브랜치의 내용, =======부터 >>>>>>> rival까지가 상대 브랜치의 내용입니다. 해결 절차: ① 파일을 연다 ② 표시 3종을 지우고 최종 내용을 쓴다 ③ git addgit commit한다.

문제 4 해답. 같은 파일의 서로 다른 부분을 고쳤을 때입니다. Git이 스스로 합칠 수 있어 Auto-mergingMerge made by the 'ort' strategy.가 뜨며 조용히 끝납니다 (3-8 실측).

완료 기준 체크리스트

  • [ ] git switch -c로 브랜치를 만들고 이동할 수 있다
  • [ ] 브랜치를 오갈 때 파일 내용이 각 우주의 마지막 상태로 바뀜을 확인했다
  • [ ] fast-forward와 merge 커밋의 차이를 말할 수 있다
  • [ ] 충돌 마커 3종을 읽고, 지우고, 통합안을 써서 해결했다
  • [ ] git log --graph --oneline으로 갈라짐과 합쳐짐을 읽을 수 있다
  • [ ] merge를 중간에 취소하는 방법(git merge --abort)을 알고 있다
  • [ ] 미션: fast-forward와 충돌 해결을 모두 재현했다

6. 흔한 실수와 해결

벽 1. switch가 거절된다

증상 (2026-09-09 실측):

error: Your local changes to the following files would be overwritten by checkout:
	story.txt
Please commit your changes or stash them before you switch branches.
Aborting

원인: 커밋하지 않은 수정이 있는데, 옮기면 그 수정이 덮일 위험이 있어 Git이 막는 것입니다.
해결: 작업 중이던 것을 커밋하거나, 당분간 치워 둘 것이라면 git stash(임시 보관함)로 넣어 둔 뒤 switch하세요. 돌아와서 git stash pop이면 복구됩니다 (2026-09-09 실측: Dropped refs/stash@{0} ...).

벽 2. 충돌 표시를 지웠는데 merge가 안 끝난다

증상: status에 여전히 "Unmerged paths"가 보입니다 (2026-09-09 실측: both modified: story.txt).

원인: 파일을 고친 뒤 git add를 안 했습니다. add가 "이 파일은 해결됐다"는 신호입니다.
해결: git add 파일명git commit. 커밋하면 merge가 완료됩니다.

벽 3. merge를 시작한 게 후회된다

증상: 충돌이 너무 많아 당장은 못 하겠습니다.
해결: git merge --abort 한 줄이면 merge 직전 상태로 깔끔하게 돌아갑니다. 실측(2026-09-09)에서 abort 직후 status는 nothing to commit, working tree clean이었습니다. "중단 버튼이 있다"는 사실 자체가 용기를 줍니다.

벽 4. 같은 파일을 고쳤는데 충돌이 안 난다

증상: 양쪽 다 같은 파일을 만졌는데 조용히 합쳐집니다.
원인: 충돌은 "같은 파일"이 아니라 "같은 줄(근처)"을 고쳤을 때만 납니다 (3-8 실측).
해결: 오류가 아니라 Git의 영리함입니다. 결과 파일을 열어 두 수정이 모두 살아 있는지 확인하세요.

벽 5. 충돌을 재현하려는데 fast-forward가 된다

증상: 일부러 만든 브랜치를 merge했는데 Fast-forward로 끝납니다.
원인: 새 브랜치를 main의 최신 커밋에서 만들어서, 상대 브랜치만 앞서 나간 모양이 되었습니다 (실측 중 실제로 겪은 실수).
해결: 새 브랜치를 공통 조상 커밋에서 만드세요 — git switch -c rival <조상해시>. 양쪽이 각자 앞으로 나아가야 충돌이 납니다.


7. 정리

오늘의 개념

개념 한 줄 설명
브랜치(branch) 커밋을 가리키는 가벼운 화살표 — 복사본이 아니다
HEAD "지금 내가 서 있는 브랜치"를 가리키는 표시
fast-forward 한쪽만 앞선 merge — 화살표를 밀기만 하면 끝
merge 커밋 양쪽이 각자 진행한 역사를 엮는, 부모 둘인 커밋
충돌(conflict) 같은 줄을 다르게 고쳐 Git이 판단을 인간에게 넘기는 상태
충돌 마커 <<<<<<< ======= >>>>>>> — 지우고 정답을 쓰면 해결

오늘의 명령어

명령 하는 일
git switch -c 이름 브랜치를 만들고 이동
git switch -c 이름 해시 특정 커밋에서 브랜치를 만들고 이동
git branch / git branch 이름 목록 보기 / 만들기만
git branch -d 이름 브랜치(화살표) 삭제
git merge 브랜치 상대 브랜치의 역사를 현재 브랜치에 합치기
git merge --abort 진행 중인 merge 취소
git stash / git stash pop 커밋 안 한 수정 임시 보관 / 꺼내기
git log --graph --oneline 역사의 모양을 지도로 보기

명령어보다 중요한 감각

충돌은 고장이 아니라 질문입니다. CONFLICT가 떠도 패닉에 빠질 필요가 없습니다 — Git이 "둘 중 무엇을 원하나요?"라고 묻는 것일 뿐이고, 답하는 절차는 파일을 열어 표시 3종을 지우고 정답을 쓰는 것뿐입니다. 그리고 언제든 git merge --abort로 물러날 수 있습니다.

협업과의 연결: GitHub에서 브랜치를 push하면 "main에 합치시겠습니까?"라고 제안하는 PR(풀 리퀘스트)을 열 수 있습니다. PR은 합치기 전에 동료가 코드를 읽고 댓글을 다는 검문소이고, 실제로 많은 취약점이 이 리뷰 단계에서 잡힙니다 — "제2의 눈"은 가장 싸고 강력한 보안 장치 중 하나입니다. 여러분은 이미 그 핵심 동작(만들고, 합치고, 충돌을 푼다)을 손에 익혔습니다.


전부 체크되면 Step 88 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.