DevOps

Git 고급 전략 - 브랜치 전략부터 위기 탈출까지

백엔드 개발자 김승원 2026. 4. 15. 09:47

들어가며

"아... force push 했는데 팀원 커밋이 날아갔어요." Git을 사용하면서 한 번쯤 심장이 쿵 내려앉는 순간을 겪어보셨을 겁니다. 혹은 merge 충돌이 200줄 넘게 발생해서 해결하는 데 반나절을 쓴 경험, release 브랜치에 잘못된 커밋이 포함되어 긴급 패치를 해야 했던 경험이 있을 것입니다.

Git은 단순한 버전 관리 도구를 넘어서, 팀의 협업 방식과 배포 전략을 결정하는 핵심 인프라입니다. 3~7년차 백엔드 개발자라면 기본적인 add/commit/push는 능숙하지만, 브랜치 전략 선택, rebase와 merge의 올바른 사용, 위기 상황 대응에서는 여전히 불안감을 느끼는 경우가 많습니다.

이 글에서는 Git Flow vs Trunk-Based Development 비교, rebase/merge 전략, interactive rebase, cherry-pick, bisect, reflog를 활용한 실수 복구까지 실무 시나리오별로 정리합니다. 읽고 나면 Git 관련 위기 상황에서 당황하지 않고 대응할 수 있을 것입니다.

1. 브랜치 전략: Git Flow vs Trunk-Based Development

Git Flow

Vincent Driessen이 제안한 모델로, 명확한 브랜치 구조를 정의합니다.

# Git Flow 브랜치 구조
main (production)     ─────●────────●────────●──────
                           │        ↑        ↑
release/1.2          ──────┤────●───┘        │
                           │    ↑            │
develop              ──●───●────●──●────●────●──────
                       ↑        ↑  ↑    ↑
feature/user-auth  ───●────●────┘  │    │
feature/payment    ────────────●───┘    │
hotfix/login-bug   ─────────────────●───┘

# 브랜치 역할
# main: 운영 환경 코드 (태그로 버전 관리)
# develop: 다음 릴리즈 통합 브랜치
# feature/*: 기능 개발 (develop에서 분기 → develop으로 병합)
# release/*: 릴리즈 준비 (develop에서 분기 → main + develop으로 병합)
# hotfix/*: 긴급 수정 (main에서 분기 → main + develop으로 병합)

Trunk-Based Development (TBD)

# Trunk-Based Development 구조
main (trunk)  ──●──●──●──●──●──●──●──●──●──●──●──
                ↑  ↑  ↑     ↑     ↑  ↑
               feat feat feat  feat feat feat
               (단수명, 1-2일 이내 병합)

# 특징
# - main 브랜치 하나에 집중
# - Feature 브랜치는 짧게 유지 (1-2일)
# - Feature Flag로 미완성 기능 제어
# - CI/CD가 강력해야 함
# - Google, Facebook, Netflix 등 대기업에서 채택

# Feature Flag 예시 (Spring Boot)
@RestController
public class PaymentController {

    @Value("${feature.new-payment-flow.enabled:false}")
    private boolean newPaymentFlowEnabled;

    @PostMapping("/payments")
    public ResponseEntity<?> processPayment(@RequestBody PaymentRequest request) {
        if (newPaymentFlowEnabled) {
            return newPaymentService.process(request);  // 새 결제 로직
        }
        return legacyPaymentService.process(request);   // 기존 결제 로직
    }
}

비교 가이드

항목 Git Flow Trunk-Based Development
배포 주기 정기 릴리즈 (2-4주) 지속적 배포 (하루 여러 번)
브랜치 수명 길다 (수일~수주) 짧다 (수시간~1-2일)
merge 충돌 빈번 (장수명 브랜치) 적음 (짧은 브랜치)
CI/CD 요구 중간 높음 (필수)
팀 규모 소~중규모 모든 규모 (특히 대규모)
적합한 상황 명확한 릴리즈 주기, 모바일 앱 웹 서비스, SaaS, 빠른 배포

2. Rebase vs Merge 전략

Merge: 이력 보존

# merge는 별도의 merge commit을 생성
# 브랜치 히스토리가 그대로 보존됨

git checkout develop
git merge feature/user-auth

# 히스토리:
#   *   Merge branch 'feature/user-auth' into develop
#   |\  
#   | * feat: Add login validation
#   | * feat: Add user authentication
#   * | fix: Update dependency
#   |/  
#   * Previous commit

Rebase: 깨끗한 히스토리

# rebase는 커밋을 base 브랜치 위로 재배치
# 선형적인 히스토리 유지

git checkout feature/user-auth
git rebase develop

# 히스토리 (rebase 후 merge):
#   * feat: Add login validation
#   * feat: Add user authentication
#   * fix: Update dependency
#   * Previous commit

# Rebase 후 Fast-Forward Merge
git checkout develop
git merge feature/user-auth  # Fast-Forward!

실무 권장 전략: Rebase + Squash Merge

# 1. Feature 브랜치에서 작업 중 develop 최신화
git checkout feature/payment
git fetch origin
git rebase origin/develop
# → 충돌 해결 후
git rebase --continue

# 2. PR 생성 후 Squash Merge로 병합 (GitHub/GitLab 설정)
# feature 브랜치의 여러 커밋을 하나로 합쳐서 develop에 병합
# → develop 히스토리가 깔끔하게 유지

# GitHub PR 설정에서 "Squash and merge" 기본으로 설정 권장

# 주의: public 브랜치(main, develop)에서는 rebase 금지!
# 다른 사람이 참조하는 커밋 히스토리가 변경되면 충돌 발생
git checkout develop
git rebase xxx  # 절대 금지!

3. Interactive Rebase로 커밋 정리

# 최근 4개 커밋 정리
git rebase -i HEAD~4

# 에디터에 표시되는 내용:
pick a1b2c3d feat: Add payment model
pick d4e5f6g fix: Fix typo in payment
pick g7h8i9j feat: Add payment service
pick j0k1l2m fix: Fix null check

# 수정 예시: typo 수정을 이전 커밋에 합치고, null check도 합침
pick a1b2c3d feat: Add payment model
fixup d4e5f6g fix: Fix typo in payment       # a1b2c3d에 합침 (메시지 버림)
pick g7h8i9j feat: Add payment service
fixup j0k1l2m fix: Fix null check             # g7h8i9j에 합침

# 결과: 깔끔한 2개 커밋
# * feat: Add payment service
# * feat: Add payment model

# Interactive Rebase 명령어
# pick (p): 커밋 사용
# reword (r): 커밋 메시지 수정
# edit (e): 커밋 수정 (amend)
# squash (s): 이전 커밋에 합침 (메시지도 합침)
# fixup (f): 이전 커밋에 합침 (이 커밋의 메시지 버림)
# drop (d): 커밋 삭제
# exec (x): 쉘 명령 실행

autosquash 활용

# commit 시 fixup 대상을 미리 지정
git commit --fixup=a1b2c3d -m "Fix typo"
# → fixup! feat: Add payment model 이라는 커밋 생성

# autosquash로 자동 정리
git rebase -i --autosquash HEAD~4
# fixup! 커밋이 자동으로 해당 커밋 아래로 이동 + fixup 설정

# Git 설정으로 autosquash 기본 활성화
git config --global rebase.autoSquash true

4. Cherry-pick: 특정 커밋만 가져오기

# 특정 커밋을 현재 브랜치에 적용
git cherry-pick abc1234

# 여러 커밋 cherry-pick
git cherry-pick abc1234 def5678 ghi9012

# 범위로 cherry-pick (시작 커밋은 미포함)
git cherry-pick abc1234..ghi9012

# 실무 시나리오: hotfix를 main에서 develop으로 가져오기
git checkout develop
git cherry-pick hotfix-commit-hash

# 충돌 발생 시
git cherry-pick abc1234
# CONFLICT!
git status              # 충돌 파일 확인
# 충돌 해결 후
git add .
git cherry-pick --continue

# cherry-pick 취소
git cherry-pick --abort

# 커밋하지 않고 변경사항만 가져오기 (여러 커밋을 하나로 합칠 때)
git cherry-pick --no-commit abc1234
git cherry-pick --no-commit def5678
git commit -m "feat: Backport payment fixes"

5. Stash 고급 활용

# 기본 stash
git stash                        # 변경사항 임시 저장
git stash pop                    # 복원 + stash 삭제
git stash apply                  # 복원 (stash 유지)

# 메시지와 함께 stash (나중에 찾기 쉽게)
git stash push -m "WIP: payment validation logic"

# 특정 파일만 stash
git stash push -m "Only service files" -- src/service/

# Untracked 파일도 포함
git stash push -u -m "Including new files"

# stash 목록 확인
git stash list
# stash@{0}: On feature/payment: WIP: payment validation logic
# stash@{1}: On develop: Experimental caching

# 특정 stash 적용
git stash apply stash@{1}

# stash 내용 미리보기
git stash show -p stash@{0}

# stash를 브랜치로 변환 (충돌 방지)
git stash branch new-branch-name stash@{0}

# 오래된 stash 정리
git stash drop stash@{2}     # 특정 stash 삭제
git stash clear              # 모든 stash 삭제

6. Git Bisect로 버그 찾기

"언제부터 이 버그가 있었지?" 수백 개의 커밋 사이에서 버그를 도입한 커밋을 이진 탐색으로 찾습니다.

# bisect 시작
git bisect start

# 현재 상태: 버그 있음
git bisect bad

# 마지막으로 정상이었던 커밋
git bisect good v1.2.0
# 또는 커밋 해시
git bisect good abc1234

# Git이 중간 커밋으로 이동
# Bisecting: 32 revisions left to test after this (roughly 5 steps)
# [def5678] feat: Update order processing

# 테스트 후 결과 표시
git bisect good   # 이 커밋에서는 정상
# 또는
git bisect bad    # 이 커밋에서는 버그 있음

# Git이 다시 중간 커밋으로 이동... 반복
# 최종 결과:
# abc9876 is the first bad commit
# commit abc9876
# Author: developer@example.com
# Date: 2025-03-15
#     refactor: Change discount calculation

# bisect 종료
git bisect reset

# 자동화: 스크립트로 bisect
git bisect start HEAD v1.2.0
git bisect run ./test.sh
# test.sh가 exit 0이면 good, exit 1이면 bad
# 자동으로 버그 커밋을 찾아줌

7. Force Push 사고 복구

"실수로 main에 force push 했어요!" 가장 무서운 Git 사고 중 하나입니다.

시나리오: force push로 팀원 커밋이 사라짐

# 사고 상황
git push --force origin main  # 실수!
# 팀원의 최신 커밋들이 원격에서 사라짐

# 복구 방법 1: reflog로 이전 상태 확인 (로컬에 이력이 남아있을 때)
git reflog
# abc1234 (HEAD -> main) HEAD@{0}: push (force): updating
# def5678 HEAD@{1}: commit: feat: My latest commit
# ghi9012 HEAD@{2}: pull: Fast-forward    ← 팀원 커밋이 포함된 상태

# 이전 상태로 복원
git reset --hard ghi9012
git push --force origin main  # 복원된 상태로 force push

# 복구 방법 2: 팀원의 로컬에서 복구
# 팀원이 아직 pull하지 않았다면 팀원 로컬에 최신 커밋이 있음
# 팀원 컴퓨터에서:
git push --force origin main

# 복구 방법 3: GitHub의 이벤트 로그
# GitHub > Settings > Audit log에서 push 이벤트 확인
# 또는 GitHub Support에 연락

사고 예방

# 1. --force 대신 --force-with-lease 사용
git push --force-with-lease origin feature/payment
# 원격 브랜치가 예상 상태와 다르면 push 거부
# → 다른 사람이 push한 커밋이 있으면 실패

# Git alias 설정 (force를 force-with-lease로 대체)
git config --global alias.pushf "push --force-with-lease"

# 2. main/develop 브랜치 보호 (GitHub)
# Settings > Branches > Branch protection rules
# - Require pull request reviews before merging
# - Require status checks to pass before merging
# - Do not allow force pushes ← 핵심!
# - Do not allow deletions

# 3. pre-push hook으로 main/develop force push 방지
# .git/hooks/pre-push
#!/bin/bash
protected_branches=("main" "develop" "master")
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')

for branch in "${protected_branches[@]}"; do
    if [ "$current_branch" = "$branch" ]; then
        echo "ERROR: Direct push to $branch is not allowed!"
        echo "Please create a pull request."
        exit 1
    fi
done

8. Reflog로 실수 되돌리기

reflog는 HEAD의 모든 이동 기록을 저장하는 Git의 안전망입니다. 거의 모든 실수를 되돌릴 수 있습니다.

실수 복구 시나리오들

# reflog 확인
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: feat: Important feature
# ghi9012 HEAD@{2}: commit: feat: Another feature
# jkl3456 HEAD@{3}: commit: feat: Base feature

# 시나리오 1: git reset --hard를 실수로 실행
git reset --hard HEAD~3       # 실수!
git reflog                    # 이전 상태 확인
git reset --hard def5678      # 복원!

# 시나리오 2: 삭제된 브랜치 복구
git branch -D feature/important  # 실수!
git reflog | grep "feature/important"
# abc1234 HEAD@{5}: checkout: moving from feature/important to develop
git checkout -b feature/important abc1234  # 복원!

# 시나리오 3: 잘못된 rebase 되돌리기
git rebase develop    # 충돌 많이 남. 잘못된 판단이었음
git reflog
# abc1234 HEAD@{0}: rebase (finish): checkout develop
# def5678 HEAD@{3}: rebase (start): checkout develop
# ghi9012 HEAD@{4}: commit: My original commit  ← 이 지점으로!
git reset --hard ghi9012  # rebase 이전 상태로 복원

# 시나리오 4: 잘못된 amend 되돌리기
git commit --amend  # 이전 커밋 메시지를 잘못 수정!
git reflog
# abc1234 HEAD@{0}: commit (amend): Wrong message
# def5678 HEAD@{1}: commit: Original correct message
git reset --soft def5678  # 원래 커밋으로 복원 (변경사항은 staging에 유지)

9. .gitattributes 활용

# .gitattributes - 파일별 Git 동작 설정

# 줄바꿈 처리 (Windows/Mac/Linux 혼합 환경)
*.java text eol=lf
*.kt text eol=lf
*.xml text eol=lf
*.yml text eol=lf
*.sh text eol=lf
*.bat text eol=crlf

# 바이너리 파일 (diff/merge 하지 않음)
*.png binary
*.jpg binary
*.jar binary
*.zip binary

# 특정 파일 diff 방식 지정
*.sql diff=sql
*.properties diff=ini

# Lock 파일은 merge 시 항상 현재 브랜치 우선
package-lock.json merge=ours
yarn.lock merge=ours
gradle.lockfile merge=ours

# 특정 파일을 export에서 제외 (git archive)
.gitignore export-ignore
.gitattributes export-ignore
tests/ export-ignore

10. 대규모 팀 Git 컨벤션

커밋 메시지 컨벤션 (Conventional Commits)

# 형식: <type>(<scope>): <description>

# type 목록
feat: 새 기능
fix: 버그 수정
refactor: 리팩토링 (기능 변경 없음)
perf: 성능 개선
test: 테스트 추가/수정
docs: 문서 변경
style: 코드 스타일 (포매팅, 세미콜론 등)
chore: 빌드, CI/CD, 의존성 등
ci: CI/CD 설정 변경

# 예시
feat(payment): Add credit card validation
fix(auth): Fix token expiration check
refactor(order): Extract discount calculation to service
perf(query): Add composite index for order search

# Breaking Change
feat(api)!: Change response format for /orders endpoint

BREAKING CHANGE: The response now wraps data in a "result" field.
Before: { "orders": [...] }
After: { "result": { "orders": [...] } }

PR 가이드라인

# PR 크기 제한
# - 변경 파일 10개 이하
# - 변경 줄 수 400줄 이하
# - 하나의 논리적 변경사항만 포함

# PR 템플릿 (.github/pull_request_template.md)
## 변경 사항
- [ ] 변경 내용을 간단히 설명해주세요

## 변경 이유
- 관련 이슈: #

## 테스트
- [ ] 유닛 테스트 추가/수정
- [ ] 로컬 테스트 완료
- [ ] 스테이징 테스트 완료

## 체크리스트
- [ ] 코드 리뷰 받음
- [ ] 문서 업데이트 (필요한 경우)
- [ ] 하위 호환성 확인

유용한 Git Alias 모음

# ~/.gitconfig
[alias]
    # 로그 시각화
    lg = log --oneline --graph --decorate --all
    ll = log --oneline -15

    # 상태 확인
    st = status -sb
    df = diff --stat

    # 브랜치 관리
    co = checkout
    cb = checkout -b
    br = branch -vv
    bd = branch -d

    # Stash
    ss = stash push -m
    sl = stash list
    sp = stash pop

    # 안전한 force push
    pushf = push --force-with-lease

    # 마지막 커밋 수정
    amend = commit --amend --no-edit

    # 현재 브랜치를 원격 최신으로 리셋
    sync = !git fetch origin && git reset --hard origin/$(git branch --show-current)

    # merge된 브랜치 정리
    cleanup = !git branch --merged | grep -v '\*\|main\|develop' | xargs -n 1 git branch -d

마치며

Git은 도구의 사용법을 넘어서 팀의 협업 문화와 직결됩니다. 이 글에서 다룬 내용을 정리합니다.

  • 브랜치 전략은 배포 주기에 맞게: 정기 릴리즈라면 Git Flow, 지속적 배포라면 Trunk-Based Development를 선택합니다. 중요한 것은 팀 전체가 같은 전략을 따르는 것입니다.
  • Rebase + Squash Merge 조합: Feature 브랜치에서 rebase로 최신화하고, PR에서 squash merge로 병합하면 깨끗한 히스토리를 유지할 수 있습니다.
  • Interactive Rebase로 커밋 정리: PR 올리기 전에 의미 있는 단위로 커밋을 정리합니다. fixup과 autosquash를 활용하면 효율적입니다.
  • bisect로 버그 찾기: 이진 탐색으로 버그를 도입한 커밋을 빠르게 찾습니다. 테스트 스크립트와 함께 자동화할 수 있습니다.
  • reflog는 최후의 안전망: 거의 모든 Git 실수를 되돌릴 수 있습니다. force push 사고도 reflog로 복구 가능합니다.
  • --force-with-lease를 습관으로: force push가 필요한 상황에서는 반드시 --force-with-lease를 사용합니다. main/develop 브랜치는 보호 규칙을 설정합니다.

Git 사고는 경험이 쌓이면 줄어들지만, 완전히 없앨 수는 없습니다. 중요한 것은 사고가 발생했을 때 당황하지 않고 체계적으로 복구하는 능력입니다. reflog를 자주 확인하는 습관, 브랜치 보호 규칙 설정, force-with-lease 사용이 이 능력의 기반이 됩니다.