git-flow 브랜치 전략,
직접 실습하며 이해하기
브랜치 전략 문서를 아무리 읽어도 실제로 커밋을 만들고 병합해보기 전에는 감이 잘 오지 않는다. 그래서 로컬에 빈 저장소를 하나 만들어 한 사이클씩 직접 돌려봤다.
2026-07-13
git-flow 브랜치 5종
브랜치역할수명
| main | 배포된 상태만 존재. 커밋마다 버전 태그가 붙음 | 영구 |
| develop | 다음 릴리스를 위해 기능이 계속 쌓이는 통합 브랜치 | 영구 |
| feature/* | 기능 하나 단위 작업. develop에서 분기해 develop으로 병합 | 임시 |
| release/* | 배포 직전 버전 고정/QA용. develop에서 분기해 main·develop 양쪽에 병합 | 임시 |
| hotfix/* | 배포된 버전의 긴급 수정. main에서 분기해 main·develop 양쪽에 병합 | 임시 |
핵심 규칙은 하나다: main은 태그가 찍힌 배포본만 담고, 모든 변경은 develop을 거쳐 흘러들어온다. release와 hotfix만 예외적으로 main에 직접 병합될 자격이 있다.
실습으로 돌려본 사이클
git init
git commit -m "Initial commit" # main
git branch develop
이후 두 번의 릴리스 사이클을 돌렸다.
- feature/hello → develop 병합
- release/1.0.0 → main(태그 v1.0.0) + develop 양쪽 병합
- hotfix/1.0.1 → main(태그 v1.0.1) + develop 양쪽 병합
- feature/login, feature/logout 동시 진행 → 각각 develop 병합
- release/1.1.0 → main(태그 v1.1.0) + develop 병합
- hotfix/1.1.1 → main(태그 v1.1.1) + develop 병합
모든 병합은 --no-ff로 강제했다. Fast-forward를 막아야 "이 지점에서 기능 브랜치가 병합됐다"는 흔적(병합 커밋)이 그래프에 남기 때문이다.
git merge --no-ff feature/hello -m "Merge feature/hello into develop"
최종 커밋 그래프
* 2a98121 (HEAD -> develop) Merge hotfix/1.1.1 into develop
|\
* \ 1c7e816 Merge release/1.1.0 into develop
|\ \
| | | * 2368e16 (tag: v1.1.1, main) Merge hotfix/1.1.1 into main
| | | |\
| | | |/
| | |/|
| | * | 8798df9 Fix login bug, bump to 1.1.1
| | |/
| | * 3684f92 (tag: v1.1.0) Merge release/1.1.0 into main
| | |\
| | |/
| |/|
| * | 220b511 Bump version to 1.1.0
|/ /
* | 8b7dad2 Merge feature/logout into develop
|\ \
| * | 6278305 Add logout feature
* | | 8d31f61 Merge feature/login into develop
|\ \ \
| |/ /
|/| |
| * | ece2788 Add login feature
|/ /
* | be777a2 Merge hotfix/1.0.1 into develop
|\ \
* \ \ d7d89f5 Merge release/1.0.0 into develop
|\ \ \
| | | * 1ceebab (tag: v1.0.1) Merge hotfix/1.0.1 into main
| | | |\
| | | |/
| | |/|
| | * | c0c6eb0 Fix critical bug, bump to 1.0.1
| | |/
| | * b2377f6 (tag: v1.0.0) Merge release/1.0.0 into main
| | |\
| | |/
| |/|
| * | be3b68b Bump version to 1.0.0
|/ /
* | fa6222f Merge feature/hello into develop
|\ \
| |/
|/|
| * 9297fda Add hello feature
|/
* 0163424 Initial commit
git log --graph --all --decorate 한 줄이면 언제든 이 그림을 다시 뽑아낼 수 있다. main 라인에는 태그가 찍힌 커밋만 존재하고, develop 라인에는 모든 병합이 순서대로 쌓여있는 것이 보인다.
느낀 점 / 팁
- feature 브랜치는 병합 즉시 삭제(git branch -d)해도 그래프에는 병합 커밋으로 흔적이 남으니 브랜치 목록을 깨끗하게 유지할 수 있다.
- release와 hotfix를 양쪽에 병합하는 걸 잊는 것이 실무에서 가장 흔한 실수다 — main에만 병합하고 develop에 반영하지 않으면, 다음 릴리스에서 방금 낸 핫픽스가 다시 사라진 것처럼 보인다.
- 확장 도구(git-flow-avh) 없이도 checkout -b, merge --no-ff, tag -a만으로 모델을 그대로 재현할 수 있어서, 명령어 하나하나가 무슨 일을 하는지 익히기에는 오히려 확장 없이 해보는 쪽이 낫다.
----------------------
merge · push 되돌리기 실습 가이드
환경 위치: \git-merge-lab\
git-merge-lab\
origin.git\ ← 원격 저장소 역할 (bare repo)
work\ ← 당신이 실습할 클론
teammate\ ← 동료 역할 클론 (이미 feature/a, feature/b를 push해둔 상태)
모든 명령은 work 폴더 안에서 실행하면 됩니다. 각 연습은 서로 독립된 브랜치에 미리 세팅해뒀으니 순서 상관없이 아무거나 먼저 해도 됩니다.
연습 1 — 충돌 중인 merge 취소하기 (git merge --abort)
현재 상태: practice/ex1-merge-abort 브랜치. app.txt의 2번째 줄을 로컬에서 이미 수정하고 커밋해뒀습니다. 동료도 같은 줄을 다르게 고쳐서 feature/a로 push해놨습니다. → 병합하면 충돌납니다.
cd C:\Users\USER\Documents\claude\git-merge-lab\work
git checkout practice/ex1-merge-abort
git status # 깨끗한 상태 확인
git merge origin/feature/a # 충돌 발생!
git status # both modified: app.txt 확인
cat app.txt # <<<<<<< HEAD ... ======= ... >>>>>>> 마커 확인
여기서 "이 병합 자체를 없던 일로" 하고 싶다면:
git merge --abort
git status # 다시 깨끗한 상태로 복구됐는지 확인
cat app.txt # 충돌 마커 없이 원래 로컬 버전 그대로인지 확인
핵심 개념: merge --abort는 충돌이 나서 아직 커밋되지 않은 병합만 취소할 수 있습니다. 병합 커밋이 이미 생성됐다면 이 명령은 쓸 수 없고(에러남), 아래 연습 2 방식을 써야 합니다.
다시 처음부터 해보고 싶다면: git reset --hard ex1-start
연습 2 — merge 커밋은 됐는데, push 전에 취소하기
현재 상태: practice/ex2-reset-before-push 브랜치. origin/feature/b를 이미 충돌 없이 병합 완료한 상태(커밋 c3a5aed)이고, 아직 origin에 push는 안 했습니다.
git checkout practice/ex2-reset-before-push
git log --oneline --graph -3 # merge 커밋이 이미 생성돼 있음을 확인
병합 자체를 무효로 하고 병합 전 상태로 완전히 돌아가려면:
git reset --hard HEAD~1
# 또는 (merge 직후에만 사용 가능한 자동 포인터)
git reset --hard ORIG_HEAD
확인:
git log --oneline -3 # feature_b.txt를 추가한 merge 커밋이 사라졌는지 확인
git status
핵심 개념
- ORIG_HEAD는 git이 merge/reset처럼 위험한 명령을 실행하기 직전 HEAD 위치를 자동 저장해두는 포인터입니다. 병합 직후라면 커밋을 몇 개 되돌려야 하는지 셀 필요 없이 바로 씁니다.
- 아직 push하지 않았다면 reset --hard로 히스토리를 자유롭게 지워도 안전합니다. 나만 보는 로컬 히스토리이기 때문입니다.
- 혹시 실수로 뭔가 날렸다면 git reflog로 모든 이전 HEAD 위치가 남아있으니 복구 가능합니다 (아래 "안전망" 참고).
다시 처음부터: git reset --hard ex2-start
연습 3 — 이미 push한 merge를 되돌리기 (가장 헷갈리는 부분)
현재 상태: practice/ex3-undo-after-push 브랜치. origin/feature/b를 병합한 커밋(5293b13)을 이미 origin에 push까지 완료했습니다. 심지어 teammate 클론도 이미 이 브랜치를 pull해간 상태입니다 (실무에서 흔히 벌어지는 상황).
git checkout practice/ex3-undo-after-push
git log --oneline --graph -3
방법 A — git revert (권장, 안전함)
이미 남에게 공유된(push된) 히스토리는 지우지 말고, "되돌리는 새 커밋"을 추가하는 게 원칙입니다.
git revert -m 1 5293b13
- -m 1: merge 커밋은 부모가 2개(원래 브랜치, 병합해온 브랜치)라서, "어느 쪽을 기준선으로 볼지" 지정해야 합니다. 1번 부모(원래 있던 브랜치)를 기준으로 되돌린다는 뜻입니다.
- 에디터가 뜨면 메시지 저장하고 닫으면 됩니다.
git log --oneline --graph -4 # revert 커밋이 새로 추가된 것 확인 (기존 커밋은 그대로 남아있음!)
git push origin practice/ex3-undo-after-push
teammate가 pull 받으면 어떻게 되는지 확인해보기:
cd ..\teammate
git checkout practice/ex3-undo-after-push
git pull
git log --oneline -4 # 충돌 없이 깔끔하게 revert 커밋을 받아옴
방법 B — reset --hard + push --force-with-lease (위험, 조건부)
히스토리 자체를 "없던 일"로 만들고 싶을 때 쓰지만, 이미 남이 pull해간 브랜치에서는 원칙적으로 쓰면 안 됩니다. 왜 위험한지 직접 확인해보세요.
cd ..\work
git reset --hard HEAD~1 # 로컬에서 merge 커밋 삭제 (아직 push 전)
git push --force-with-lease origin practice/ex3-undo-after-push
이제 teammate 쪽에서:
cd ..\teammate
git fetch origin
git status # "브랜치가 갈라졌습니다(diverged)" 혹은 origin이 뒤에 있다는 경고 확인
teammate 로컬에는 이미 지워진 커밋이 남아있고, 다음에 pull하면 rebase/merge 충돌이나 예상치 못한 되감기가 발생할 수 있습니다. 이게 바로 "이미 push한 걸 reset으로 지우면 안 되는" 이유입니다 — 나 혼자 쓰는 히스토리가 아니게 된 순간, 지우는 대신 되돌리는 커밋(revert)을 쌓아야 합니다.
다시 처음부터: git reset --hard ex3-start (단, ex3-start 태그가 없다면 origin.git에는 원본이 남아있으니 git fetch && git reset --hard origin/practice/ex3-undo-after-push로 복구 — 이미 force-push 해버렸다면 git reflog에서 원래 커밋 해시(5293b13)를 찾아 git reset --hard 5293b13으로 복구)
안전망 — git reflog
뭘 하다가 "어? 커밋이 사라졌다" 싶을 때는 언제나:
git reflog
reset, merge, checkout 등 HEAD가 움직인 모든 기록이 남아있습니다 (기본 90일). 원하는 시점의 해시를 찾아서:
git reset --hard <reflog에서-찾은-해시>
로 복구할 수 있습니다. reflog는 오직 로컬에만 있는 안전망이라, push해서 원격/동료에게 넘어간 히스토리를 지운 경우는 되돌리기 전에 이미 다른 사람 컴퓨터에 퍼져있을 수 있다는 점이 다릅니다. 그래서 "이미 공유된 히스토리는 revert, 아직 나만 아는 히스토리는 reset"이 기본 원칙입니다.
한눈에 정리
상황아직 push 안 함이미 push해서 공유됨
| 충돌 중인 merge 취소 | git merge --abort | (해당 없음 — push는 커밋 완료 후에나 가능) |
| merge 커밋을 없던 일로 | git reset --hard HEAD~1 (또는 ORIG_HEAD) | git revert -m 1 <merge-commit> 후 git push |
| 실수 복구 | git reflog → git reset --hard <해시> | 동일 + 이미 남이 pull했다면 revert로 한 번 더 |
| 히스토리 강제로 되감기 | 자유롭게 가능 | push --force-with-lease — 남이 이미 pull했으면 위험, 팀 합의 없이 지양 |
'프로그래밍 > Git' 카테고리의 다른 글
| Windows 소스트리(Sourcetree) 설치하기 (0) | 2024.07.04 |
|---|---|
| 윈도우용 Git설치하기 (0) | 2024.07.04 |
| .gitignore 파일 설정과 .idea파일 제거 - Git rm -cached (0) | 2023.01.05 |
| Git 불필요한 파일들은 Commit 제외 시키기 (0) | 2018.09.10 |