콘텐츠로 이동

Git Flow & Branching

분류: Layer 5 - 플랫폼 엔지니어링 & 자동화

Git 브랜치 전략 (Git Flow / GitHub Flow / Trunk-Based Development)

섹션 제목: “Git 브랜치 전략 (Git Flow / GitHub Flow / Trunk-Based Development)”

팀이 코드를 어떻게 나누고, 합치고, 배포할지를 정의하는 브랜치 운영 규칙으로, 협업 충돌을 줄이고 안정적인 배포를 가능하게 하는 개발 워크플로우다.


브랜치 전략이 없으면 생기는 문제

섹션 제목: “브랜치 전략이 없으면 생기는 문제”

개발자 5명이 동시에 main 브랜치에 직접 커밋한다고 상상해보자. A가 결제 기능을 개발하는 도중 B가 인증 모듈을 수정하고, 그 사이 C가 긴급 버그를 고쳐야 한다. 결과는 예측 불가능한 충돌, 테스트되지 않은 코드의 배포, 롤백의 악몽이다.

  • 병렬 개발 안정성: 서로 다른 기능을 독립된 공간에서 개발
  • 배포 품질 보장: 리뷰, 테스트를 거친 코드만 main에 합류
  • 롤백 용이성: 문제가 생기면 어느 지점으로 되돌릴지 명확
  • 이력 추적: “언제 어떤 기능이 왜 들어갔는지” 히스토리 파악

브랜치 전략 선택은 팀의 속도와 안정성 모두에 직결된다. DORA의 2021 State of DevOps Report는 신뢰성 목표를 달성한 elite performer가 낮은 성과 그룹보다 Trunk-Based Development를 사용할 가능성이 2.3배 높다고 보고했다(Google Cloud State of DevOps 2021). DORA의 TBD capability 문서도 같은 결론을 운영 지표로 바꾼다: 활성 브랜치 3개 이하, trunk 병합 하루 1회 이상, 코드 프리즈와 별도 통합 단계 제거가 권장 상태다(DORA Trunk-Based Development). 즉 “어떤 전략이 멋진가”가 아니라 “우리 팀이 충돌·리뷰·배포를 얼마나 자주 흡수할 수 있는가”의 문제다.


3. 선행 방식의 한계와 브랜치 전략의 등장

섹션 제목: “3. 선행 방식의 한계와 브랜치 전략의 등장”

브랜치 전략은 Git이 생기면서 갑자기 발명된 “규칙 놀이”가 아니다. 선행 방식은 크게 두 가지 한계를 가졌다. 첫째, CVS/Subversion 같은 중앙집중식 버전 관리에서는 브랜치와 머지가 고급 기능처럼 취급되어 자주 쓰기 어려웠고, Vincent Driessen은 Git Flow 원문에서 Git 이전 세계의 merge/branch가 “가끔만 하는 두려운 작업”으로 여겨졌다고 회고한다(A successful Git branching model). 둘째, 브랜치를 아예 안 쓰고 모두가 공유 trunk/main에 직접 올리면 충돌은 빨리 드러나지만, 리뷰·테스트·릴리즈 경계가 없어서 깨진 코드가 그대로 배포 경로에 들어간다.

Git은 브랜치 생성과 병합 비용을 낮췄지만, “가볍게 만들 수 있다”는 사실만으로 운영 질서가 생기지는 않는다. Git Flow는 이 문제를 develop·release·hotfix라는 역할 분리로 풀었다. GitHub Flow는 별도 통합 브랜치를 줄이고 PR·status check·branch protection으로 기본 브랜치의 품질 경계를 세운다. TBD는 반대로 브랜치 수명을 줄이고 Feature Flag로 코드 배포와 기능 노출을 분리해 긴 통합 단계를 없앤다. 세 전략은 모양은 달라도 모두 lineage_oneliner의 표현처럼 병렬 개발의 충돌·배포 실패 위험을 격리 → 검증 → 통합하는 메커니즘이다.


Git Flow는 대형 마트처럼 동작한다. 상품이 **진열대(main)**에 오르려면 입고 창고(develop)검품실(release)진열대(main) 순서를 거쳐야 한다. 급히 필요한 상품은 검품실을 건너뛰고 **응급 코너(hotfix)**를 통해 바로 진열대에 오른다.

main ──●──────────────────────●── (v1.0)
│ │
│ hotfix/bug ──┤── (긴급 패치)
│ │
develop ─────●──●──●──●──●──────────●──
│ │
feature/login ────●────────┘
feature/payment └──●──●──┘
release/1.0 ──────────────────●──● (QA, 버전 태깅)
브랜치역할생성 기준삭제 시점
main배포된 프로덕션 코드최초 1회영구 유지
develop통합 개발 브랜치main에서 파생영구 유지
feature/*기능 단위 개발develop에서 파생develop 병합 후 삭제
release/*배포 준비 (QA, 버그 픽스)develop에서 파생main 병합 후 삭제
hotfix/*프로덕션 긴급 패치main에서 파생main + develop 병합 후 삭제
Terminal window
# 1. feature 브랜치 생성 (develop에서 파생)
git checkout develop
git checkout -b feature/user-auth
# 기능 개발 후
git add .
git commit -m "feat: JWT 인증 구현"
# 2. develop에 병합
git checkout develop
git merge --no-ff feature/user-auth
git branch -d feature/user-auth
# 3. release 브랜치 생성
git checkout -b release/1.0.0
# 버전 정보 업데이트, QA 버그 픽스
git commit -m "chore: 버전 1.0.0 설정"
# 4. main에 병합 (배포)
git checkout main
git merge --no-ff release/1.0.0
git tag -a v1.0.0 -m "Release 1.0.0"
# 5. develop에도 병합 (release에서 수정된 내용 반영)
git checkout develop
git merge --no-ff release/1.0.0
git branch -d release/1.0.0

예상 출력:

Switched to branch 'develop'
Switched to a new branch 'feature/user-auth'
[feature/user-auth abc1234] feat: JWT 인증 구현
3 files changed, 87 insertions(+)
Switched to branch 'develop'
Merge made by the 'ort' strategy.
Deleted branch feature/user-auth (was abc1234).
Terminal window
# 프로덕션 긴급 버그 발생
git checkout main
git checkout -b hotfix/null-pointer-fix
git commit -m "fix: 결제 시 null pointer 예외 수정"
# main과 develop 모두에 반영
git checkout main
git merge --no-ff hotfix/null-pointer-fix
git tag -a v1.0.1 -m "Hotfix 1.0.1"
git checkout develop
git merge --no-ff hotfix/null-pointer-fix
git branch -d hotfix/null-pointer-fix

”왜 —no-ff 옵션이 중요한가” — 머지 전략의 원리

섹션 제목: “”왜 —no-ff 옵션이 중요한가” — 머지 전략의 원리”

Git Flow에서 --no-ff(no fast-forward)는 단순한 습관이 아니라 이력 추적성을 위한 필수 옵션이다.

Fast-forward 머지 (--ff, 기본값):
main: ──A──B──C──D── (feature 커밋이 main에 직접 이어붙음)
→ "언제 어떤 feature가 main에 들어왔는지" 경계가 보이지 않음
No-fast-forward 머지 (--no-ff):
main: ──A──────────M── (머지 커밋 M이 경계를 만듦)
╲ ╱
feature: ──B──C──D
→ git log --graph로 "이 기능이 언제 합쳐졌는지" 명확하게 추적 가능
→ git revert M 한 번으로 기능 전체를 되돌릴 수 있음

fast-forward로 머지하면 feature 브랜치의 커밋이 main에 직접 이어붙어서, 나중에 “이 기능을 통째로 되돌리고 싶다”면 커밋 하나하나를 일일이 revert해야 한다. --no-ff는 머지 커밋을 남겨서 하나의 revert로 기능 전체를 안전하게 되돌릴 수 있게 해준다.

Git 공식 문서 기준으로도 --ff는 가능할 때 branch pointer만 이동하고 merge commit을 만들지 않지만, --no-ff는 fast-forward가 가능한 상황에서도 merge commit을 만든다(git-merge documentation). 따라서 Git Flow에서 --no-ff는 예쁘게 보이기 위한 옵션이 아니라 “release에 포함된 기능 묶음”이라는 감사 단위를 남기는 장치다.

📖 더 보기: A successful Git branching model (nvie.com) — Git Flow 원저자가 —no-ff를 필수로 권장하는 이유 설명 (입문)

장점:

  • 릴리즈 버전 관리가 명확하다 (v1.0, v1.1, v2.0 체계적 관리)
  • 여러 버전을 동시에 지원해야 하는 경우 적합 (모바일 앱, 패키지 라이브러리)
  • QA 팀이 별도로 있는 조직에 적합

단점:

  • 브랜치가 많아 복잡하다
  • developreleasemain 단계가 많아 배포까지 시간이 걸린다
  • CI/CD를 구축해도 배포 속도 향상이 제한적이다
  • 원저자 Vincent Driessen 본인도 2020년에 “웹 앱에는 Git Flow가 맞지 않는다”고 인정했다

GitHub Flow는 편의점처럼 단순하다. 물건이 필요하면 발주(feature 브랜치 생성)입고 검수(PR + 리뷰)바로 진열(main 머지 + 배포). 창고 단계 없이 빠르게 순환한다.

main ──●──────────────────────────────●── (항상 배포 가능)
│ │
feature/ └──●──●──●── (PR 생성) ────── ┘
↑ ↑
커밋 코드 리뷰

GitHub Flow는 딱 두 가지 브랜치 유형만 존재한다:

  1. main: 항상 배포 가능한 상태를 유지
  2. feature/* (또는 fix/*, chore/*): 모든 작업 브랜치
1. main에서 브랜치 생성
git checkout -b feature/add-search
2. 작업 후 원격에 push
git push -u origin feature/add-search
3. Pull Request 생성 (GitHub UI)
- 제목: "feat: 상품 검색 기능 추가"
- 리뷰어 지정
- CI/CD 자동 트리거
4. 코드 리뷰 + 수정
5. main에 Merge
6. 자동 배포 (CI/CD)
Terminal window
# 1. 브랜치 생성
git checkout main
git pull origin main
git checkout -b feature/product-search
# 2. 개발
git add .
git commit -m "feat: 상품명 기반 검색 API 추가"
git push -u origin feature/product-search
# 3. PR 생성 (GitHub CLI 사용 시)
gh pr create \
--title "feat: 상품 검색 기능 추가" \
--body "## 변경 내용\n- SearchController 추가\n- SearchService 구현" \
--reviewer team-lead
# 4. 리뷰 승인 후 머지
gh pr merge --squash
# 5. 로컬 정리
git checkout main
git pull origin main
git branch -d feature/product-search

예상 출력:

Switched to a new branch 'feature/product-search'
[feature/product-search def5678] feat: 상품명 기반 검색 API 추가
5 files changed, 142 insertions(+)
Branch 'feature/product-search' set up to track remote branch 'feature/product-search' from 'origin'.
https://github.com/team/repo/pull/42
✓ Pull request #42 successfully merged and closed

“왜 GitHub Flow에서 main은 항상 배포 가능해야 하는가” — 지속적 배포의 전제 조건

섹션 제목: ““왜 GitHub Flow에서 main은 항상 배포 가능해야 하는가” — 지속적 배포의 전제 조건”

GitHub Flow의 핵심 규율은 **“main = 배포 가능”**이다. 이 규율이 무너지면 전체 전략이 붕괴한다.

main이 배포 불가능한 상태일 때 벌어지는 일:
1. 개발자 A가 feature 브랜치를 main에서 파생
2. 그런데 main에 이미 깨진 코드가 있음
3. A의 feature 브랜치도 시작부터 깨진 상태
4. A가 PR을 올려도 CI가 실패 → "원래 실패하는 거예요"
5. CI 실패가 정상이 되면 아무도 CI 결과를 신뢰하지 않음
6. 결국 리뷰 없이 머지 → 품질 붕괴
main이 항상 배포 가능할 때:
1. 어떤 시점에서든 main을 배포해도 안전
2. feature 브랜치가 CI 실패 → "내 코드에 문제가 있다"로 확신
3. CI 결과를 팀 전체가 신뢰
4. 긴급 상황에 즉시 main을 배포해서 대응 가능

이 원칙을 지키려면 Branch Protection Rules에서 “Require status checks to pass”가 필수다. GitHub 공식 문서에 따르면 protected branch의 required status check는 successful, skipped, neutral 상태여야 병합을 허용하며, strict 모드에서는 base branch 최신화까지 요구한다(GitHub protected branches). 단, strict 모드를 끄면 CI가 통과한 뒤 다른 PR이 먼저 머지되어도 현재 PR을 다시 검증하지 않을 수 있다. 이 경우 화면에는 “green check”가 남아 조용히 안전해 보이지만, 병합 후 main에서만 조합 충돌이 터지는 silent failure가 된다. PR이 하루 20개 이상 몰리는 저장소라면 strict status check나 merge queue를 켜고, 실패한 경우에는 gh pr update-branch <PR번호> 또는 GitHub UI의 Update branch로 최신 base 위에서 CI를 다시 돌린 뒤 병합해야 한다.

📖 더 보기: GitHub Flow Guide — GitHub Docs — GitHub Flow의 공식 가이드. main 브랜치 보호 전략 포함 (입문)

장점:

  • 배우기 쉽고 관리 포인트가 적다
  • PR이 협업 + 리뷰 + 배포 트리거를 모두 처리
  • 소규모 팀(2~10명)에 가장 적합
  • CI/CD와 자연스럽게 연동

단점:

  • 여러 버전을 동시 지원하기 어렵다
  • main이 항상 배포 가능해야 한다는 규율이 필요하다
  • 대규모 팀에서는 PR 병목이 생길 수 있다

TBD는 실시간 뉴스 방송처럼 동작한다. 기자(개발자)들이 하루에도 여러 번 뉴스(커밋)를 방송(main)에 올린다. 미완성 기사는 방송하지 않는 것이 아니라, **자막으로 “준비 중”(Feature Flag)**이라고 표시한 채로 방송한다.

모든 개발자가 하루에 1~2회 이상 main 브랜치에 직접 병합한다. 브랜치를 만들더라도 최대 1~2일 안에 병합한다. 미완성 기능은 Feature Flag로 감춘다.

main ──●──●──●──●──●──●──●──● (하루에 수십 번 커밋)
↑ ↑ ↑
개발자A B C (모두 main에 직접 or 단기 브랜치로)
// Nest.js 예시: Feature Flag로 미완성 기능 숨기기
@Injectable()
export class ProductService {
constructor(private readonly featureFlagService: FeatureFlagService) {}
async getProducts(userId: string) {
// Flag가 켜진 사용자에게만 새 검색 기능 노출
if (await this.featureFlagService.isEnabled("new-search", userId)) {
return this.newSearchProducts();
}
return this.legacySearchProducts();
}
}
// 환경변수 기반 간단 구현
const FEATURE_FLAGS = {
"new-search": process.env.FEATURE_NEW_SEARCH === "true",
"payment-v2": process.env.FEATURE_PAYMENT_V2 === "true",
};
Terminal window
# 작은 단위로 자주 커밋 (큰 PR 대신 작은 커밋들)
git checkout main
git pull origin main
# 기능의 일부만 구현 (완성되지 않아도 OK, Flag로 숨김)
git add src/search/search.service.ts
git commit -m "feat: 검색 서비스 기본 구조 추가 (flag: new-search)"
git push origin main
# CI가 자동으로 빌드/테스트 실행
# 통과하면 자동 배포

TBD가 머지 충돌을 줄이는 이유 (동작 원리 심화)

섹션 제목: “TBD가 머지 충돌을 줄이는 이유 (동작 원리 심화)”

왜 브랜치 수명이 짧을수록 충돌이 적은가?

머지 충돌은 두 브랜치가 같은 코드 영역을 각자 다르게 수정할 때 발생한다. 브랜치가 2일이면 베이스(main)와 2일치 차이만 존재한다. 브랜치가 2주면 14일치 차이가 쌓인다. 충돌 가능성은 수명에 비례한다.

권장 수명은 추정이 아니라 두 공식 출처가 일치한다. TBD 공식 가이드는 단기 feature 브랜치를 “최대 2일, 그 이상이면 long-lived 위험 신호”로 정의한다(trunkbaseddevelopment.com/short-lived-feature-branches). DORA의 trunk-based development capability 페이지는 같은 원리를 다른 측면으로 측정한다: 동시 활성 브랜치 ≤ 3개, 최소 하루 1회 trunk 머지, 브랜치 수명 “수 시간”을 권장(dora.dev/devops-capabilities/technical/trunk-based-development). 두 수치는 모두 “불일치가 누적되기 전에 흡수한다”는 한 원리의 다른 KPI다.

Git Flow (브랜치 수명 2주):
main: A──B──C──D──E──F──G──H──I──J──K──L──M──N (14일간 14개 커밋)
feature: └──────────────────────────────────────● (병합 시 14개와 충돌 가능)
TBD (브랜치 수명 1일):
main: A──B──C──D──E──F──G──H──I──J──K──L──M──N
└─● ← 1일치 = 최대 2~3개 커밋과 충돌 가능

Feature Flag가 필요한 이유

TBD에서는 기능이 완성되기 전에도 main에 병합한다. 그러면 미완성 코드가 프로덕션에 배포된다. **Feature Flag는 “코드는 배포됐지만 기능은 꺼져있다”**는 상태를 만드는 장치다.

배포(Deploy) ≠ 기능 출시(Release)
코드 배포 기능 출시
주문 서비스 v2.0 ──────── Flag ON(특정 팀만) ──── Flag ON(전체)
main 병합 1주 후 2주 후
TBD: 미완성이어도 배포
Flag: OFF 상태로 숨겨짐

이 “통합 단위(코드 머지)와 노출 단위(사용자 트래픽)를 분리한다”는 원리는 Git 워크플로 바깥에서도 같은 형태로 작동한다. 표 한 장으로 외우기보다 같은 원리가 어디서 어떻게 변형되는지 짚어두는 편이 새 플랫폼에 옮겨갈 때 유용하다.

  • K8s 카나리 배포 (Argo Rollouts): 새 버전 Pod가 클러스터에 들어가지만 setWeight: 10으로 트래픽 10%에만 노출된다. Deploy(Pod 등록)와 Release(트래픽 가중치)가 분리된다 — Feature Flag의 인프라 계층 버전. (출처: Argo Rollouts canary docs)
  • DB 스키마 expand/contract 마이그레이션: 새 컬럼 추가(expand) → 코드 두 경로 공존 → 신뢰 확인 후 옛 컬럼 제거(contract). Flag 단계적 정리와 동일한 두 단계 — “코드 안에 두 진실을 잠시 공존시킨다.”
  • A/B 실험 / Dark Launch: 같은 코드, 다른 노출 비율. rolloutPercentage가 곧 실험군 배정. 기능 출시가 아니라 가설 검증이 목적이라는 점만 다르다.

세 가지 모두 공통 KPI는 “롤백 시간”이다 — Flag OFF 한 줄, setWeight: 0 한 줄, contract 단계 보류 한 줄로 끝나야 원리가 작동한다. 그렇지 않다면 분리가 안 된 것이고, “통합과 노출이 같은 단위에 묶여 있다”는 신호다.

// AWS AppConfig 기반 Feature Flag (실무 패턴)
import {
AppConfigDataClient,
GetLatestConfigurationCommand,
} from "@aws-sdk/client-appconfigdata";
@Injectable()
export class FeatureFlagService {
private readonly client = new AppConfigDataClient({
region: "ap-northeast-2",
});
async isEnabled(flagName: string, userId?: string): Promise<boolean> {
try {
const command = new GetLatestConfigurationCommand({
ConfigurationToken: process.env.APPCONFIG_TOKEN,
});
const response = await this.client.send(command);
const config = JSON.parse(
new TextDecoder().decode(response.Configuration),
);
// 사용자별 점진적 롤아웃 (userId 해시로 10% 사용자에게만 노출)
const flag = config.flags[flagName];
if (!flag?.enabled) return false;
if (!userId || flag.rolloutPercentage >= 100) return flag.enabled;
// 사용자 ID 해시로 일관된 그룹 배정
const hash = userId
.split("")
.reduce((acc, c) => acc + c.charCodeAt(0), 0);
return hash % 100 < flag.rolloutPercentage;
} catch {
return false; // Flag 서비스 장애 시 기능 OFF (안전한 기본값)
}
}
}
// AppConfig 설정 예시 (JSON)
{
"flags": {
"new-search": {
"enabled": true,
"rolloutPercentage": 10 // 전체 사용자의 10%에게만 노출
},
"payment-v2": {
"enabled": false // 완전히 꺼짐
}
}
}

📖 더 보기: Trunk Based Development 공식 사이트 — Feature Flag의 유형(Release Toggle, Experiment Toggle 등)과 관리 전략 (중급)

장점:

  • 배포 빈도 최대화 (하루 수십 번도 가능)
  • 머지 충돌 최소화 (브랜치 수명이 짧아서)
  • CI/CD 파이프라인과 완벽하게 연동
  • DORA 지표 기준 고성능 팀의 표준

단점:

  • 강력한 코드 리뷰 문화가 전제 조건
  • 자동화 테스트 커버리지가 없으면 위험
  • Feature Flag 관리 비용이 발생 (오래된 Flag는 기술 부채가 됨)
  • 주니어가 많은 팀에서는 도입이 어렵다

브랜치 보호 규칙은 팀이 정한 브랜치 전략을 강제로 지키게 해주는 GitHub 설정이다.

GitHub Repository → Settings → Branches → Branch protection rules → Add rule
설정효과권장 여부
Require a pull request before merging직접 push 차단, PR만 허용항상 권장
Require approvals (최소 1명)최소 N명의 리뷰 승인 필요소규모 팀: 1명, 대규모: 2명
Require status checks to passCI 테스트 통과 후에만 머지 가능항상 권장
Require conversation resolution모든 리뷰 코멘트 해결 후 머지권장
Restrict who can push특정 사람/팀만 push 허용대규모 팀 권장
Require linear historySquash 또는 Rebase만 허용 (Merge Commit 불가)히스토리 정리에 유용
Do not allow bypassing관리자도 규칙 적용실수 방지
Terminal window
# GitHub CLI로 브랜치 보호 규칙 설정
gh api repos/{owner}/{repo}/branches/main/protection \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["ci/build","ci/test"]}' \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":1}' \
--field restrictions=null

PR을 main에 합칠 때 세 가지 방식이 있다. 어떤 방식을 선택하느냐에 따라 git 이력이 달라진다.

시나리오: feature 브랜치에 커밋 3개 (A, B, C)가 있고 main에 머지

# Merge Commit (--no-ff)
main: ──1──2──────────────M──
╲ ╱
feature: ──A──B──C
결과: main에 A, B, C, M (머지 커밋) 모두 보임
# Squash Merge
main: ──1──2────────ABC──
결과: main에 A+B+C가 하나의 커밋(ABC)으로 압축
feature 브랜치의 개별 커밋은 사라짐
# Rebase
main: ──1──2──A'──B'──C'──
결과: A, B, C가 main 위에 재배치 (새 해시값)
선형 이력 유지
방식이력추천 상황주의사항
Merge Commit복잡, 원본 보존오래 유지할 브랜치, 감사 추적 필요git log가 복잡해짐
Squash Merge깔끔, 정보 손실feature 브랜치 완료 후 삭제할 때git bisect 어려워짐
Rebase선형, 해시 변경공유되지 않은 브랜치공유 브랜치에는 절대 사용 금지
Repository Settings → Pull Requests
☑ Allow merge commits
☑ Allow squash merging
☑ Allow rebase merging

팀 컨벤션을 통일하고 싶다면 두 개를 비활성화하고 하나만 남긴다.

# .github/workflows/main.yml (GitHub Actions 예시)
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npm test # PR 단계: 테스트만
- run: npm run lint
deploy:
needs: test
if: github.ref == 'refs/heads/main' # main 머지 시만 배포
runs-on: ubuntu-latest
steps:
- name: Deploy to Production
run: |
echo "Deploying to AWS..."
# aws ecs update-service ...

BackOps / Nest.js + AWS 스택 기준으로 각 전략의 현실적 적합성을 분석한다.

소규모 팀 (2~5명) + 빠른 배포 필요GitHub Flow 추천

  • PR 기반 리뷰로 품질 유지
  • main 머지 즉시 AWS ECS/Lambda 자동 배포
  • 복잡한 브랜치 관리 오버헤드 없음
  • 실패 신호: PR이 평균 2일 넘게 열려 있거나, main 보호 규칙 없이 “일단 머지 후 수정”이 반복되면 GitHub Flow가 아니라 사실상 무규칙 feature branch가 된다. 이때는 main에 required status check와 최소 1명 approval을 먼저 강제한다.

중규모 팀 (5~15명) + 버전 릴리즈 존재Git Flow 추천

  • 배포 주기가 주 1회 이상이라면 release 브랜치가 QA를 돕는다
  • hotfix 흐름이 긴급 패치를 안전하게 처리
  • 실패 신호: release 브랜치가 1주 이상 살아 있고, develop에 새 기능이 계속 쌓여 release와 develop의 차이가 커진다면 통합 비용이 뒤로 미뤄진 것이다. 이 경우 release scope를 줄이거나 GitHub Flow로 단순화한다.

고성능 팀 + 강한 CI/CD 문화Trunk-Based Development 추천

  • 자동화 테스트 커버리지 80% 이상 필요
  • Feature Flag 인프라 (AWS AppConfig, LaunchDarkly 등)
  • 도입 사례 (검증 가능한 규모): Google은 약 35,000명 개발자가 단일 monorepo trunk에서 작업하며 일일 수만 건 commit, Facebook은 모바일 앱을 일일 릴리즈한다(출처: trunkbaseddevelopment.com). 두 사례 모두 강한 테스트 + flag 인프라가 전제였고, 이것이 빠진 채 TBD만 도입한 팀이 회귀하는 패턴이 7.5절의 “문제 6”이다.
  • 전환 후 분기별로 추적할 측정 지표 — PR open → merge 평균 시간 < 2일 (TBD 공식 권장 한계), 동시 활성 long-lived 브랜치 < 3개 (DORA 기준), 머지 충돌 발생 PR 비율 < 5%, 배포 후 1시간 내 롤백 비율 < 0.5%. 한 분기 안에 이 네 지표가 안정화되지 않으면 GitHub Flow로 회귀하는 편이 안전하다 — TBD는 진척 신호가 없으면 “노출되지 않은 미완성 코드의 누적”이라는 가장 비싼 실패 모드로 빠진다.

BackOps 같은 Nest.js + AWS 운영 도구라면 기본값은 GitHub Flow가 가장 현실적이다. 예를 들어 개발자 4명, ECS 배포 주 3~5회, 모바일 앱처럼 구버전 동시 지원이 없는 서비스라면 main 보호 규칙 + PR squash merge + staging 자동 배포로 시작한다. 이후 PR 평균 수명이 1일 안쪽이고, 테스트가 핵심 API 경로를 충분히 막아주며, AWS AppConfig 같은 Flag 시스템으로 신규 기능을 OFF 배포할 수 있을 때만 TBD로 좁힌다. 반대로 고객사별 설치형 버전이나 장기 지원 브랜치가 생기면 Git Flow의 release/hotfix 분리가 다시 비용보다 이득이 커진다.

Nest.js 프로젝트에서의 브랜치 네이밍

섹션 제목: “Nest.js 프로젝트에서의 브랜치 네이밍”
Terminal window
# 추천 브랜치 네이밍 컨벤션
feature/TICKET-123-user-auth # Jira 티켓 번호 포함
fix/TICKET-456-null-pointer # 버그 수정
chore/update-nestjs-version # 유지보수
release/v1.2.0 # 릴리즈 (Git Flow 사용 시)
hotfix/payment-crash # 긴급 패치

기준Git FlowGitHub FlowTrunk-Based Dev
브랜치 수5종 (main, develop, feature, release, hotfix)2종 (main, feature)1종 (main) + 단기 브랜치
배포 빈도낮음 (주 1회~월 1회)중간 (PR 완료마다)높음 (하루 수십 회)
팀 규모중대형 (10명+)소중형 (2~15명)규모 무관, 문화 중요
학습 곡선높음낮음중간 (규율 필요)
CI/CD 친화성낮음높음매우 높음
버전 동시 지원쉬움어려움어려움
머지 충돌 위험높음 (브랜치 수명 길다)중간낮음 (수명 짧다)
적합한 제품패키지, 앱SaaS, 웹서비스클라우드 네이티브

”우리 팀에 맞는 전략” 판단 기준

섹션 제목: “”우리 팀에 맞는 전략” 판단 기준”
1. 배포 주기가 얼마나 자주인가?
- 월 1회 이하 → Git Flow
- 주 1~3회 → GitHub Flow
- 하루 여러 번 → TBD
2. 여러 버전을 동시에 지원해야 하는가?
- YES → Git Flow
- NO → GitHub Flow or TBD
3. 자동화 테스트 커버리지가 충분한가?
- 낮음 (< 50%) → Git Flow or GitHub Flow (안전망 필요)
- 높음 (> 80%) → TBD 도입 가능
4. 팀에 코드 리뷰 문화가 있는가?
- 없거나 약함 → Git Flow (프로세스로 보완)
- 있음 → GitHub Flow or TBD

문제 1: main에 직접 push했을 때 브랜치 보호 규칙이 막는 경우

섹션 제목: “문제 1: main에 직접 push했을 때 브랜치 보호 규칙이 막는 경우”

증상:

remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Required status check "ci/test" is failing.
To https://github.com/team/repo.git
! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'https://github.com/team/repo.git'

원인: Branch Protection Rules에서 PR 없이 직접 push를 차단하거나, CI 테스트가 통과하지 않으면 머지가 불가능하도록 설정됨

해결 방법:

Terminal window
# 1. 새 브랜치로 이동
git checkout -b fix/my-direct-commit
# 2. PR을 통해 머지 요청
git push -u origin fix/my-direct-commit
gh pr create --title "fix: 직접 푸시한 변경사항" --base main
# (임시 방편: 관리자 권한으로 "Allow administrators" 비활성화 - 비권장)

문제 2: feature 브랜치가 오래되어 develop과 충돌이 심각한 경우

섹션 제목: “문제 2: feature 브랜치가 오래되어 develop과 충돌이 심각한 경우”

증상:

CONFLICT (content): Merge conflict in src/auth/auth.service.ts
CONFLICT (content): Merge conflict in src/user/user.module.ts
Automatic merge failed; fix conflicts and then commit the result.

원인: 오래된 feature 브랜치가 develop 브랜치와 많이 벌어져서 같은 파일을 동시에 수정한 경우

해결 방법:

Terminal window
# develop 최신 상태로 feature 브랜치 업데이트 (rebase 권장)
git checkout feature/old-branch
git fetch origin
git rebase origin/develop
# 충돌 발생 시 파일 직접 수정 후
git add src/auth/auth.service.ts
git rebase --continue
# 해결 불가 시 abort 후 merge로 대체
git rebase --abort
git merge origin/develop

근본 해결: 브랜치 수명을 짧게 유지한다. feature 브랜치는 최대 3~5일 이내에 병합을 목표로 설계한다.


문제 3: Squash Merge 후 같은 브랜치를 계속 사용할 때 충돌

섹션 제목: “문제 3: Squash Merge 후 같은 브랜치를 계속 사용할 때 충돌”

증상:

hint: You have divergent branches and need to specify how to reconcile them.
fatal: Need to specify how to reconcile divergent branches.

원인: Squash Merge는 feature 브랜치의 커밋들을 하나로 합쳐서 새 커밋을 만든다. 따라서 feature 브랜치의 원본 커밋과 main의 squash 커밋은 Git이 “다른 변경사항”으로 인식한다. 이 상태에서 feature 브랜치를 계속 사용하면 이미 반영된 내용을 다시 충돌로 처리한다.

해결 방법:

Terminal window
# Squash Merge 후에는 반드시 브랜치를 삭제하고 새로 생성
git branch -d feature/old-branch # 로컬 삭제
git push origin --delete feature/old-branch # 원격 삭제
# 추가 작업이 있다면 새 브랜치 생성
git checkout -b feature/continuation main

문제 4: Feature Flag 정리 안 해서 기술 부채 누적 (TBD)

섹션 제목: “문제 4: Feature Flag 정리 안 해서 기술 부채 누적 (TBD)”

증상:

코드베이스에 Feature Flag 분기가 50개 이상 존재한다.
어떤 Flag가 아직 사용 중이고 어떤 것이 이미 100% 릴리즈된 건지 모른다.
새 기능 개발 시 기존 Flag와 조합이 꼬여서 예상치 못한 버그 발생.

원인: Feature Flag는 TBD에서 미완성 코드를 숨기기 위한 임시 장치다. 기능이 100% 릴리즈되면 Flag와 분기 코드를 제거해야 하는데, 이를 방치하면 코드 경로가 기하급수적으로 늘어난다 (Flag 2개 = 4가지 경로, 10개 = 1024가지 경로).

해결 방법:

Terminal window
# 1. Flag 인벤토리 관리 (스프레드시트 또는 자동화)
# flag_name | created_date | owner | status | removal_target_date
# new-search | 2024-01-15 | @팀원A | 100% rolled out | 2024-02-15
# 2. 릴리즈 완료된 Flag의 코드 정리
# ❌ 정리 전
if (await featureFlagService.isEnabled('new-search', userId)) {
return this.newSearchProducts();
}
return this.legacySearchProducts();
# ✅ 정리 후 (100% 릴리즈 확인 후)
return this.newSearchProducts();
# + legacySearchProducts() 메서드 삭제
# 3. CI에서 만료된 Flag 감지 (날짜 기반)
# jest나 lint 규칙으로 만료일이 지난 Flag가 있으면 빌드 실패 처리

📖 더 보기: Feature Flags Best Practices — Flagsmith — Feature Flag 생명주기 관리와 정리 전략 (중급)


문제 5: Git Flow에서 hotfix를 develop에 반영하지 않아 버그 재발

섹션 제목: “문제 5: Git Flow에서 hotfix를 develop에 반영하지 않아 버그 재발”

증상: main에 긴급 패치를 배포했는데, 다음 release에서 같은 버그가 다시 나타남

원인: hotfix 브랜치를 main에만 병합하고 develop에 반영하지 않음. develop → release → main 흐름에서 hotfix가 덮어씌워짐

해결 방법:

Terminal window
# hotfix 완료 시 반드시 두 브랜치 모두 병합
git checkout main
git merge --no-ff hotfix/critical-fix
git tag -a v1.0.1
git checkout develop # ← 이 단계를 절대 빠뜨리지 말 것
git merge --no-ff hotfix/critical-fix
git branch -d hotfix/critical-fix

예방: Git Flow 도구(git flow) 사용 시 자동으로 두 브랜치에 병합한다.

Terminal window
git flow hotfix start critical-fix
# 수정 후
git flow hotfix finish critical-fix # main + develop 자동 병합

문제 6: Trunk-Based Development 전환 실패 패턴

섹션 제목: “문제 6: Trunk-Based Development 전환 실패 패턴”

증상:

Git Flow → TBD로 전환 직후 main 브랜치가 자주 깨진다.
빌드 실패율이 오히려 증가하고, 팀원이 불안해한다.
"역시 Git Flow로 돌아가자"는 의견이 나온다.

원인: TBD는 3가지 인프라 없이는 작동하지 않는다.

  1. CI가 모든 커밋에서 자동 실행되지 않음: 깨진 코드가 발견 없이 합류
  2. Feature Flag 없이 미완성 기능이 main에 머지됨: 반쪽짜리 기능이 사용자에게 노출
  3. 테스트 커버리지 < 60%: 회귀 감지 불가, 신뢰할 수 없는 main

TBD 전환 전 Pre-requisite 체크 (수치 기준):

조건기준미충족 시
CI 실행 시간< 10분 (이상적으로 < 5분)개발자가 확인 없이 푸시 → 깨진 빌드
핵심 로직 커버리지≥ 70%회귀 감지 신뢰도 부족
Feature Flag 시스템존재해야 함미완성 코드를 숨길 방법이 없음
PR 수명평균 < 2일충돌 해결 비용이 장점을 상쇄
Terminal window
# 3가지 미충족 시 중간 단계: GitHub Flow(단순 Feature Branch)로 먼저 전환
# Git Flow → GitHub Flow → TBD 순서가 안전한 전환 경로

  • maindevelop 브랜치가 분리되어 있는가?
  • feature 브랜치는 항상 develop에서 파생하는가?
  • hotfix 브랜치 병합 후 develop에도 반영하는가?
  • release 브랜치에서 QA 후 main에 태그를 남기는가?
  • main 브랜치는 항상 배포 가능한 상태인가?
  • 모든 변경사항이 PR을 통해 병합되는가?
  • Branch Protection Rules가 설정되어 있는가?
  • CI/CD가 PR 단계에서 자동 실행되는가?
  • 리뷰어 지정 및 승인 프로세스가 있는가?
  • 자동화 테스트 커버리지가 충분한가? (70% 이상 권장)
  • Feature Flag 시스템이 있는가? (환경변수, 외부 서비스 등)
  • 브랜치 수명이 1~2일 이내로 유지되는가?
  • 팀에 코드 리뷰 문화가 확립되어 있는가?
  • CI가 모든 커밋에 즉시 실행되는가?
  • 팀 전원이 합의한 브랜치 네이밍 컨벤션이 있는가?
  • 커밋 메시지 컨벤션이 있는가? (Conventional Commits 등)
  • Squash / Merge Commit / Rebase 방식이 통일되어 있는가?
  • Branch Protection Rules이 main (그리고 develop) 브랜치에 적용되어 있는가?

키워드설명
main / master프로덕션에 배포된 안정 코드 브랜치
developGit Flow의 통합 개발 브랜치
feature branch기능 단위 작업 브랜치
release branch배포 준비 브랜치 (QA, 버전 태깅)
hotfix branch프로덕션 긴급 패치 브랜치
trunkTBD에서 main을 부르는 다른 이름
Feature Flag코드 배포와 기능 노출을 분리하는 패턴
Pull Request (PR)코드 리뷰 + 브랜치 병합 요청
Branch Protection RulesGitHub에서 브랜치에 적용하는 보호 규칙
Squash Merge여러 커밋을 하나로 압축 후 병합
Rebase커밋을 다른 브랜치 위에 재배치
Merge Commit두 브랜치의 이력을 하나로 합치는 커밋
CI/CD자동 빌드/테스트/배포 파이프라인
DORA metricsDevOps 성과 측정 지표 (배포 빈도, 리드타임 등)


Terminal window
# git-flow CLI 도구 설치 (macOS)
brew install git-flow-avh
# 새 저장소에서 git flow 초기화
mkdir git-flow-practice && cd git-flow-practice
git init
git flow init
# 기본값으로 Enter 연타

예상 출력:

Which branch should be used for bringing forth production releases?
- master
Branch name for production releases: [master] main
Which branch should be used for integration of the "next release"?
- develop
Branch name for "next release" development: [develop]
How to name your supporting branch prefixes?
Feature branches? [feature/]
Bugfix branches? [bugfix/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []
Gitflow configuration stored in .git/config
Terminal window
# feature 브랜치 시작
git flow feature start my-feature

예상 출력:

Switched to a new branch 'feature/my-feature'
Summary of actions:
- A new branch 'feature/my-feature' was created, based on 'develop'
- You are now on branch 'feature/my-feature'
Terminal window
# 파일 작성 후 커밋
echo "# My Feature" > feature.md
git add feature.md
git commit -m "feat: my feature 추가"
# feature 브랜치 완료
git flow feature finish my-feature

예상 출력:

Switched to branch 'develop'
Updating 1a2b3c4..5d6e7f8
Fast-forward
feature.md | 1 +
1 file changed, 1 insertion(+)
Deleted branch feature/my-feature (was 5d6e7f8).
Summary of actions:
- The feature branch 'feature/my-feature' was merged into 'develop'
- Feature branch 'feature/my-feature' has been locally deleted
- You are now on branch 'develop'

검증 체크 — 실습이 의도대로 됐는지 직접 확인:

Terminal window
git log --oneline --graph --all # 머지 커밋과 분기 흔적이 보여야 함
git branch # feature/my-feature가 사라졌는지
git config --get gitflow.branch.develop # 결과: develop (없으면 init이 저장 안 됨)

위 명령에서 Fast-forward만 보이고 머지 커밋(Merge branch ...)이 없다면, git flow feature finish--no-ff가 활성화되지 않은 것이다 — git config gitflow.feature.finish.no-ff true 후 실습을 다시 실행하면 머지 커밋 경계가 남는다(4-1절의 “왜 —no-ff인가” 원리 확인).

변형 실험 (다음 단계):

  1. release 흐름 직접 재현: git flow release start 0.1.0 → 한 커밋 추가 → git flow release finish 0.1.0git tag 출력에 0.1.0, git log main에 머지 커밋이 보이는지 확인.
  2. 7.5절 “문제 5”(hotfix를 develop에 빠뜨려 버그 재발) 재현: main에 직접 한 커밋 만들고 git flow hotfix start fix-xfinish 실행. git log develop --oneline | grep fix-x로 develop에도 동일 커밋이 들어왔는지 확인 — git flow가 자동으로 두 브랜치에 병합해주는 메커니즘을 눈으로 확인하는 단계.
  3. 수명 측정 습관: git for-each-ref --format='%(refname:short) %(committerdate:relative)' refs/heads/feature/* 로 로컬 feature 브랜치 나이를 확인. 2일을 넘긴 브랜치가 있다면 위에서 인용한 TBD/DORA 권장과 어떻게 다른지 비교한다.

실습 2: 현재 브랜치 보호 규칙 확인 (GitHub CLI)

섹션 제목: “실습 2: 현재 브랜치 보호 규칙 확인 (GitHub CLI)”
Terminal window
# 현재 저장소의 브랜치 보호 규칙 조회
gh api repos/{owner}/{repo}/branches/main/protection \
--jq '{
required_reviews: .required_pull_request_reviews.required_approving_review_count,
status_checks: .required_status_checks.contexts,
enforce_admins: .enforce_admins.enabled
}'

예상 출력:

{
"required_reviews": 1,
"status_checks": ["ci/build", "ci/test"],
"enforce_admins": true
}

Terminal window
# 테스트 저장소 생성
mkdir merge-practice && cd merge-practice
git init
echo "initial" > README.md
git add . && git commit -m "initial commit"
# Merge Commit 방식
git checkout -b feature/a
echo "feature a" > a.txt
git add . && git commit -m "feat: A 기능 1"
echo "update" >> a.txt
git add . && git commit -m "feat: A 기능 2"
git checkout main
git merge --no-ff feature/a -m "Merge: feature/a"
git log --oneline --graph

예상 출력:

* f3a1b2c Merge: feature/a
|\
| * 9d8e7f6 feat: A 기능 2
| * 1a2b3c4 feat: A 기능 1
|/
* 0000001 initial commit
Terminal window
# Squash Merge 방식
git checkout -b feature/b
echo "feature b" > b.txt
git add . && git commit -m "feat: B 기능 1"
echo "update" >> b.txt
git add . && git commit -m "feat: B 기능 2"
git checkout main
git merge --squash feature/b
git commit -m "feat: B 기능 (squash)"
git log --oneline --graph

예상 출력:

* a1b2c3d feat: B 기능 (squash) ← 2개 커밋이 1개로 압축됨
* f3a1b2c Merge: feature/a
|\
| * 9d8e7f6 feat: A 기능 2
| * 1a2b3c4 feat: A 기능 1
|/
* 0000001 initial commit

Git Flow는 버전 관리 명확성, GitHub Flow는 단순성과 속도, Trunk-Based Development는 최고의 배포 빈도를 추구하며, 팀 규모·배포 주기·테스트 문화에 따라 최적의 전략을 선택해야 한다.