콘텐츠로 이동

Git Flow & Branching

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

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

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

브랜치 전략은 팀이 코드를 어디에서 나누고, 어떤 검증을 거쳐, 언제 기본 브랜치에 합칠지 정한 협업 규칙이다. 이름 규칙이 아니라 충돌, 리뷰, 배포, 롤백의 경계를 설계하는 방법이다.

Git을 혼자 쓸 때는 브랜치가 “작업 공간”처럼 보인다. 팀에서 쓰기 시작하면 브랜치는 “누가 어떤 위험을 언제 흡수할 것인가”를 정하는 경계가 된다. Git Flow, GitHub Flow, Trunk-Based Development는 이 경계를 서로 다르게 둔다.


브랜치 전략 문서는 용어가 비슷해서 처음 읽을 때 헷갈리기 쉽다. 아래 용어를 먼저 잡고 뒤 절을 읽으면 세 전략의 차이가 명확해진다.

용어첫 정의왜 필요한가
브랜치 전략브랜치를 만들고 병합하고 배포하는 팀 규칙충돌, 리뷰, 릴리즈 경계를 우연이 아니라 규칙으로 만든다
main현재 제품의 기준이 되는 기본 브랜치배포, 긴급 복구, 새 작업 시작점의 기준점이다
trunkTBD에서 부르는 단일 통합 브랜치, 보통 main모두가 자주 합류하는 중심선을 강조하는 말이다
feature branch기능, 버그 수정, 실험을 위해 만든 짧은 작업 브랜치미완성 변경을 기본 브랜치에서 잠시 분리한다
release branch배포 후보를 안정화하기 위해 만든 임시 브랜치새 기능 유입을 막고 QA, 버전 태깅, 마지막 버그 픽스를 모은다
hotfix branch운영 장애를 빠르게 고치기 위해 main에서 파생한 브랜치다음 정규 릴리즈를 기다리지 않고 프로덕션을 먼저 복구한다
PR(Pull Request)한 브랜치의 변경을 다른 브랜치에 합치자고 요청하는 리뷰 단위코드 리뷰, CI 결과, 토론, 승인 기록이 모이는 경계다
status checkPR 또는 커밋에 붙는 자동 검증 결과테스트, 빌드, 린트가 통과했는지 병합 전에 확인한다
branch protection특정 브랜치에 직접 push, 미검증 병합을 막는 규칙팀 규칙을 선의가 아니라 시스템 제약으로 만든다
merge queue여러 PR을 최신 main 조합 위에서 순서대로 검증한 뒤 병합하는 큐green check가 낡은 base에서 나온 결과일 때 생기는 조합 실패를 줄인다
feature flag코드 배포와 사용자 노출을 분리하는 조건 스위치미완성 코드를 main에 넣어도 기능은 OFF로 숨길 수 있다
deploy코드를 환경에 배포하는 행위사용자가 볼지와 별개로 실행 가능한 코드가 서버에 올라간다
release기능을 사용자에게 노출하는 행위flag, 트래픽 가중치, 앱스토어 릴리즈 등으로 통제된다
merge commit두 브랜치 이력을 합쳤다는 커밋기능 묶음, 릴리즈 묶음, revert 단위를 남긴다
squash mergePR의 여러 커밋을 하나로 압축해 병합하는 방식main 이력을 읽기 쉽게 만들지만 개별 커밋 맥락은 줄어든다
rebase내 커밋을 다른 base 위에 다시 올리는 방식선형 이력을 만들지만 커밋 해시가 바뀐다

중요한 구분은 deployrelease다. GitHub Flow에서는 main에 머지하면 바로 배포되는 팀이 많다. TBD에서는 더 나아가 미완성 코드도 main에 자주 합치되, feature flag로 사용자 노출을 따로 제어한다. Git Flow는 반대로 release/* 브랜치로 배포 후보를 따로 안정화한다.


3. 선행 한계 -> 브랜치 전략이 등장한 문제

섹션 제목: “3. 선행 한계 -> 브랜치 전략이 등장한 문제”

브랜치 전략은 Git 명령어를 예쁘게 정리하려고 생긴 규칙이 아니다. 팀 개발에서 반복적으로 터지는 세 가지 문제가 배경이다.

첫째, 충돌 경계가 없다. 개발자 5명이 같은 main에 직접 push하면 충돌은 빨리 드러날 수 있지만, 어떤 변경이 어떤 기능에 속하는지, 어떤 변경을 되돌려야 하는지 알기 어렵다. 결제 기능이 반쯤 들어간 상태에서 인증 수정과 긴급 버그 픽스가 섞이면 커밋 그래프는 남아도 의사결정 경계가 사라진다.

둘째, 리뷰와 검증 경계가 없다. CI/CD 문서에서 본 것처럼 자동화 테스트는 빠른 피드백을 주지만, 어떤 변경이 테스트 대상인지 정해져야 한다. PR이 없거나 branch protection이 없으면 “테스트가 실패했지만 급하니까 머지”가 가능해지고, 그 순간 status check는 신뢰 신호가 아니라 참고 메모가 된다.

셋째, 릴리즈 경계가 없다. 웹 서비스처럼 언제든 배포 가능한 제품은 main을 곧 배포 후보로 둘 수 있다. 반대로 모바일 앱, SDK, 고객사별 설치형 제품처럼 여러 버전을 동시에 지원해야 하면 “이번 릴리즈에 포함되는 것”과 “다음 릴리즈로 넘어가는 것”을 분리해야 한다.

Git은 브랜치 생성과 병합 비용을 낮췄다. 하지만 브랜치를 쉽게 만들 수 있다는 사실만으로 협업 질서가 생기지는 않는다. Git Flow는 main, develop, release, hotfix 역할을 나눠 릴리즈 경계를 강하게 만든다. GitHub Flow는 PR, status check, branch protection으로 main 품질 경계를 세운다. Trunk-Based Development(TBD)는 브랜치 수명을 극단적으로 줄이고 feature flag로 배포와 노출을 분리한다.


4. 핵심 철학: 격리 -> 검증 -> 통합

섹션 제목: “4. 핵심 철학: 격리 -> 검증 -> 통합”

세 전략은 모양이 다르지만 같은 질문에 답한다.

  1. 격리: 미완성 변경을 어디까지 분리할 것인가?
  2. 검증: 어떤 자동/수동 검증을 통과해야 합칠 수 있는가?
  3. 통합: 충돌과 배포 위험을 언제 흡수할 것인가?

Git Flow는 격리를 길게 가져간다. 기능은 feature/*, 다음 릴리즈 후보는 develop, 안정화는 release/*, 운영 긴급 패치는 hotfix/*로 나눈다. 통합 비용은 늦게 오지만, 릴리즈 경계가 명확하다.

GitHub Flow는 격리를 짧게 가져간다. 모든 작업은 main에서 짧은 feature branch로 갈라졌다가 PR 검증을 거쳐 main으로 돌아온다. 릴리즈 브랜치를 기본으로 두지 않기 때문에 main이 항상 배포 가능해야 한다.

TBD는 격리를 더 짧게 가져간다. 모든 개발자가 trunk에 자주 합친다. 미완성 기능은 브랜치가 아니라 feature flag로 숨긴다. 통합 비용을 거의 매일 지불하는 대신, 긴 통합 단계와 대형 충돌을 피한다.

브랜치 전략을 고르는 일은 “어떤 이름이 더 표준인가”가 아니다. 우리 팀이 통합 비용을 초반에 자주 낼 수 있는지, 아니면 릴리즈 시점에 늦게 크게 내는 편이 맞는지 정하는 일이다.


5. Git Flow: 릴리즈 경계를 강하게 잡는 전략

섹션 제목: “5. Git Flow: 릴리즈 경계를 강하게 잡는 전략”

Git Flow는 maindevelop이라는 두 장기 브랜치를 두고, 기능 개발·릴리즈 준비·긴급 패치를 각각 다른 임시 브랜치로 처리하는 전략이다. Vincent Driessen의 “A successful Git branching model”에서 널리 알려졌고, 원문도 Git 이전의 branch/merge가 두려운 작업으로 취급되던 맥락을 설명한다.

Git Flow 브랜치 역할

main

프로덕션에 배포된 코드와 릴리즈 태그가 남는 영구 브랜치다.

운영 장애 시 마지막 정상 버전과 rollback 기준이 필요할 때.

develop

다음 릴리즈 후보가 될 기능들이 모이는 통합 개발 브랜치다.

새 기능은 계속 합치되 production 배포와는 분리해야 할 때.

feature/*

개별 기능이나 수정 작업을 위해 develop에서 파생하는 짧은 브랜치다.

기능 개발 중 develop과 production 경계를 건드리지 않으려 할 때.

release/*

QA, 버전 태깅, 배포 전 버그 픽스만 담는 임시 안정화 브랜치다.

이번 버전에 들어갈 범위를 얼리고 새 기능 유입을 막아야 할 때.

hotfix/*

프로덕션 긴급 패치를 main에서 파생해 main과 develop에 모두 되돌려 넣는 브랜치다.

운영 장애를 먼저 복구하고 다음 릴리즈에서도 같은 버그가 재발하지 않게 해야 할 때.

5.2 메커니즘: 릴리즈 기차를 만든다

섹션 제목: “5.2 메커니즘: 릴리즈 기차를 만든다”

Git Flow의 핵심은 develop에서 계속 기능을 모으다가, 특정 시점에 release/1.4.0 같은 브랜치를 잘라 “이번 배포 기차”를 만든다는 점이다.

main A───────────────M1──────────────M2
╲ ↑ ↑
develop D0──F1──F2────R1────F3──F4────H'
╲ ╲ ╲
feature/login L1──L2 ╲
feature/billing B1──B2
release/1.4.0 r1──r2
hotfix/pay-crash h1

이 그래프에서 release/1.4.0이 만들어진 뒤 develop에는 F3, F4가 계속 들어올 수 있다. 하지만 release/1.4.0에는 QA 중 발견된 r1, r2 같은 안정화 변경만 들어간다. 배포가 끝나면 release branch는 main에 병합되고 태그가 붙는다. release 중 발견된 수정은 develop에도 다시 반영해야 다음 릴리즈에서 사라지지 않는다.

5.3 Worked example: hotfix를 develop에 되돌리지 않으면

섹션 제목: “5.3 Worked example: hotfix를 develop에 되돌리지 않으면”

상황을 작게 만들어보자.

  1. main에는 v1.0.0이 배포되어 있다.
  2. develop에는 다음 릴리즈용 결제 리팩터링이 들어가 있다.
  3. 운영에서 null pointer 장애가 발생해 hotfix/payment-nullmain에서 만든다.
  4. hotfix를 main에만 병합하고 develop에는 반영하지 않는다.
  5. 다음 주 develop -> release -> main 흐름으로 v1.1.0을 배포한다.

이때 v1.1.0develop에서 나온다. develop에 hotfix가 없었다면 v1.0.1에서 고친 버그가 v1.1.0에서 다시 나타난다. Git Flow의 hotfix 규칙은 형식이 아니라 “장기 브랜치가 두 개라서 같은 패치를 두 흐름에 모두 넣어야 한다”는 구조적 필요에서 나온다.

--no-ff는 no fast-forward merge를 뜻한다. fast-forward는 Git이 별도 merge commit 없이 브랜치 포인터만 앞으로 옮기는 병합이다. --no-ff는 fast-forward가 가능한 상황에서도 merge commit을 만들어 “이 기능 묶음이 여기서 들어왔다”는 경계를 남긴다.

Fast-forward:
main: A──B──C──D
↑ feature 커밋이 main에 직접 이어붙는다
No-fast-forward:
main: A────────M
╲ ╱
feature: B──C──D
↑ M이 기능 묶음의 경계가 된다

Git Flow에서 이 경계는 유용하다. release에 어떤 feature 묶음이 들어갔는지 감사할 수 있고, 특정 기능 전체를 되돌릴 때 merge commit 하나를 기준으로 삼을 수 있다. 반대로 작은 PR이 매우 많은 웹 서비스에서 모든 PR을 merge commit으로 남기면 main 이력이 장황해질 수 있다. 그래서 GitHub Flow 팀은 squash merge를 기본으로 고르는 경우가 많다.

Git Flow는 긴 릴리즈 기차가 실제로 필요한 팀에서 빛난다.

조건Git Flow가 유리한 이유
모바일 앱, SDK, 설치형 제품처럼 여러 버전이 동시에 살아 있다release/*, hotfix/*, tag가 버전별 지원 경계를 만든다
QA 단계가 별도 조직/일정으로 운영된다release branch가 새 기능 유입을 막고 안정화 변경만 받는다
배포 주기가 주간, 격주, 월간처럼 묶여 있다”이번 기차에 타는 변경”을 명확히 정할 수 있다
고객사별 패치나 장기 지원 브랜치가 필요하다main/tag/release branch가 감사와 지원 기준이 된다

Git Flow가 안정적이라는 말은 충돌이 적다는 뜻이 아니다. 오히려 feature branch나 release branch가 길어지면 통합 비용은 뒤로 밀린다. Git Flow의 장점은 충돌을 없애는 것이 아니라 릴리즈 범위와 긴급 패치 경계를 명확히 하는 데 있다.

Git Flow 원저자도 2020년 업데이트에서 웹 애플리케이션처럼 지속 배포가 자연스러운 제품에는 Git Flow가 적합하지 않을 수 있다고 설명했다. 이 문장은 “Git Flow가 낡았다”가 아니라 “제품의 릴리즈 모델이 다르면 브랜치 전략도 달라져야 한다”로 읽어야 한다.

  • release branch가 팀이 정한 안정화 기간을 넘겼고 develop과 차이가 계속 커진다.
  • release stabilizing 중인데 새 기능이 계속 release branch에 들어온다.
  • hotfix를 main에만 넣고 develop이나 다음 release branch에 되돌리지 않는다.
  • develop이 자주 깨져 feature branch의 시작점으로 신뢰받지 못한다.
  • --no-ff merge commit은 남지만 어떤 기능 묶음인지 PR 설명과 태그가 부실하다.

6. GitHub Flow: PR과 자동 검증으로 main을 지키는 전략

섹션 제목: “6. GitHub Flow: PR과 자동 검증으로 main을 지키는 전략”

GitHub Flow는 main과 짧은 feature branch만 두는 단순한 전략이다. 모든 작업은 main에서 브랜치를 만들고, PR을 열고, 리뷰와 status check를 통과한 뒤 main으로 병합한다. 별도의 develop이나 release/* 브랜치를 기본으로 두지 않는다.

main A──────────────M1──────────────M2
╲ ╱ ╲ ╱
feature/search s1──s2────── feature/pay p1
PR + CI PR + CI

이 전략의 핵심 문장은 “main은 항상 배포 가능해야 한다”이다. main이 깨져 있으면 모든 feature branch가 깨진 시작점에서 갈라지고, CI 실패가 “원래 실패하는 것”이 된다. 한 번 그렇게 되면 status check는 품질 경계로 작동하지 않는다.

6.2 PR은 토론 창이 아니라 변경 단위다

섹션 제목: “6.2 PR은 토론 창이 아니라 변경 단위다”

PR(Pull Request)은 코드 리뷰 UI 이상의 의미를 가진다. PR은 한 번에 검증할 변경 단위다. PR이 너무 크면 리뷰어는 의도를 놓치고, CI가 실패해도 어느 변경이 원인인지 찾기 어렵다. PR이 너무 작아도 여러 PR이 서로 의존하면서 merge 순서 문제가 생긴다.

좋은 PR 단위는 “리뷰어가 한 번에 의도와 위험을 설명할 수 있는 변경”이다. 예를 들어 상품 검색 API 추가는 검색 endpoint, service, test를 포함할 수 있다. 하지만 검색 API + 결제 리팩터링 + Dockerfile 변경은 서로 다른 실패 원인을 한 PR에 묶는다.

status check는 CI가 PR 또는 commit에 붙이는 결과다. 예를 들어 ci/test, ci/build, lint, typecheck가 성공해야 병합할 수 있게 만들 수 있다. branch protection은 이 결과를 병합 조건으로 강제한다.

보호 규칙막는 실패학습 포인트
Require pull request before mergingmain 직접 push리뷰와 CI가 우회되지 않는다
Require approvals혼자 승인하고 병합최소 한 명이 의도를 검토한다
Require status checks to pass깨진 빌드/테스트 병합자동 검증이 병합 게이트가 된다
Require conversation resolution미해결 리뷰 코멘트 방치토론의 끝을 명확히 한다
Require linear historymerge commit 난립squash/rebase 중심 이력을 강제한다
Do not allow bypassing관리자 예외 남용긴급 상황에서도 규칙 우회가 보편화되지 않는다

6.4 Silent failure: 초록 체크가 항상 최신 검증은 아니다

섹션 제목: “6.4 Silent failure: 초록 체크가 항상 최신 검증은 아니다”

GitHub Flow에서 자주 놓치는 실패가 있다. PR A와 PR B가 모두 같은 main 커밋 위에서 CI를 통과했다고 하자. PR A가 먼저 병합되면 PR B의 green check는 낡은 base에서 나온 결과다. strict status check가 꺼져 있거나 merge queue가 없으면 PR B는 최신 main + B 조합으로 다시 검증되지 않은 채 병합될 수 있다.

시간 1: main = A
PR-1 = A + 검색 변경 CI PASS
PR-2 = A + 인증 변경 CI PASS
시간 2: PR-1 병합
main = A + 검색 변경
시간 3: PR-2 병합
검증된 조합: A + 인증 변경
실제 배포 조합: A + 검색 변경 + 인증 변경

두 PR이 서로 다른 파일을 바꾸면 보통 괜찮다. 하지만 검색 endpoint가 인증 middleware 이름에 의존한다면 main에서만 실패한다. strict mode는 PR을 최신 base로 업데이트한 뒤 다시 CI를 돌리게 하고, merge queue는 여러 PR을 순서대로 최신 조합에서 검증한다.

PR이 하루에 몇 개 안 들어오는 저장소라면 Update branch 후 재검증으로 충분할 수 있다. PR이 하루 수십 개씩 들어오거나 같은 모듈을 자주 건드린다면 merge queue가 더 안전하다. 숫자는 절대 기준이 아니라 “동시에 열린 PR이 서로 자주 겹치는가”를 보는 관찰 기준이다.

조건이유
SaaS, 웹 서비스처럼 main에서 자주 배포한다별도 release branch보다 PR 검증 경계가 단순하다
팀 규모가 작거나 중간이고 PR 리뷰 문화가 있다복잡한 브랜치 교육 없이 품질 경계를 만들 수 있다
CI/CD가 PR마다 빠르게 돈다main 보호 규칙이 실제 피드백으로 작동한다
여러 구버전을 장기 지원하지 않는다release/hotfix 브랜치 복잡도를 줄일 수 있다
  • main이 깨져 있는데도 “나중에 고치자”며 새 PR을 계속 병합한다.
  • required status check가 없거나 실패해도 관리자 권한으로 자주 우회한다.
  • PR 평균 수명이 길어져 feature branch가 사실상 long-lived branch가 된다.
  • squash merge 후 같은 브랜치를 계속 재사용해 divergent branch 충돌이 반복된다.
  • strict status check나 merge queue 없이 같은 영역의 PR이 대량 병합된다.

7. Trunk-Based Development: 통합 비용을 매일 지불하는 전략

섹션 제목: “7. Trunk-Based Development: 통합 비용을 매일 지불하는 전략”

Trunk-Based Development(TBD)는 모든 개발자가 하나의 trunk, 보통 main에 매우 자주 통합하는 전략이다. 브랜치를 만들 수는 있지만 수명이 짧아야 한다. 미완성 기능은 오래 숨겨둔 브랜치가 아니라 feature flag로 감춘다.

trunk/main A──B──C──D──E──F──G──H
↑ ↑ ↑ ↑ ↑
작고 자주 합쳐지는 변경

TBD의 철학은 “통합은 고통스럽기 때문에 미루지 않는다”다. 통합을 2주 뒤로 미루면 2주치 변경과 한 번에 맞춰야 한다. 통합을 하루에 여러 번 하면 작은 차이를 계속 흡수한다. 충돌이 사라지는 것이 아니라 충돌 하나하나가 작아진다.

7.2 수치 감각: 브랜치 수명과 충돌 면적

섹션 제목: “7.2 수치 감각: 브랜치 수명과 충돌 면적”

충돌 확률을 정확히 계산할 수는 없지만, 감각은 만들 수 있다. 팀이 하루에 main에 10개 커밋을 넣고, 한 feature branch가 10일 동안 열려 있다고 하자. 병합 시점에는 최대 100개 커밋이 같은 코드베이스에 먼저 들어와 있다. 같은 branch가 1일만 살아 있으면 비교해야 할 변경은 대략 10개 커밋이다.

long-lived branch:
main: A──1──2──3──4──5──...──100
feature: ╲────────────────────────F
병합 시 100개 변경과 한 번에 맞춘다
short-lived branch:
main: A──1──2──3──4──5──...──100
╲─F
병합 시 최근 몇 개 변경과 맞춘다

DORA의 trunk-based development capability 문서는 활성 브랜치 수를 적게 유지하고 trunk에 자주 병합하는 관점을 제시한다. TBD 공식 사이트도 short-lived feature branch를 강조한다. 이 수치들은 “항상 오늘의 최신 정답”이라기보다 브랜치 전략을 측정 가능한 운영 습관으로 바꾸는 참고 기준으로 읽는 편이 안전하다.

실무에서 관찰할 만한 기준은 다음과 같다.

신호읽는 법
평균 PR open -> merge 시간이 2일을 자주 넘는다TBD보다는 GitHub Flow의 단기 feature branch에 가깝다
동시에 오래 열린 feature branch가 3개를 계속 넘는다trunk와 분기들의 차이가 커지고 있다
하루에 한 번도 trunk/main에 합치지 못하는 날이 반복된다통합 비용을 매일 흡수한다는 TBD 리듬이 깨지고 있다
같은 파일 충돌이 PR마다 반복된다작업 쪼개기나 소유권 경계가 불분명하다
CI가 10분 이상 걸려 피드백을 기다리지 않는다자주 통합할 동기가 사라진다

7.3 Feature flag: deploy와 release를 분리한다

섹션 제목: “7.3 Feature flag: deploy와 release를 분리한다”

TBD는 미완성 기능도 main에 넣을 수 있어야 한다. 그런데 미완성 기능이 사용자에게 노출되면 안 된다. feature flag는 이 간극을 메운다.

코드 통합 코드 배포 기능 출시
main merge -> production deploy -> flag ON
↑ ↑ ↑
통합 단위 실행 환경 사용자 노출

작은 예를 보자.

async function searchProducts(query: string, userId: string) {
if (await flags.enabled("new-search", userId)) {
return searchV2(query);
}
return searchV1(query);
}

이 코드는 searchV2가 완성되기 전에도 main에 들어갈 수 있다. 기본 flag가 OFF라면 사용자 대부분은 여전히 searchV1을 쓴다. 일부 내부 사용자에게만 1%, 10%, 50%, 100%처럼 노출 비율을 올릴 수 있다. 롤백도 code revert가 아니라 flag OFF로 시작한다.

7.4 Release toggle과 실험 toggle을 구분한다

섹션 제목: “7.4 Release toggle과 실험 toggle을 구분한다”

feature flag는 모두 같은 목적이 아니다.

flag 유형목적수명
Release toggle미완성 기능을 숨기고 점진 노출한다짧아야 한다
Experiment toggleA/B 실험으로 사용자 반응을 비교한다실험 기간만 유지한다
Ops toggle장애 시 특정 기능을 끄거나 우회한다운영 정책에 따라 오래 갈 수 있다
Permission toggle특정 고객/권한에 기능을 제공한다제품 권한 모델의 일부가 될 수 있다

TBD에서 특히 위험한 것은 release toggle 방치다. 기능 출시가 끝났는데도 release toggle이 남으면 코드 경로가 둘로 유지된다. release toggle 10개가 독립적으로 남아 있으면 이론상 조합은 2의 10승, 즉 1024가지다. 모든 조합을 테스트하지 못하므로 “어떤 고객 조합에서만 깨지는 버그”가 생긴다.

조건이유
CI가 빠르고 모든 commit/PR에서 자동 실행된다자주 통합해도 깨진 코드를 빠르게 잡는다
핵심 경로 테스트가 신뢰된다main을 항상 배포 가능하게 유지한다
feature flag 인프라와 정리 루틴이 있다미완성 코드 노출과 flag 부채를 통제한다
PR/commit 단위가 작다통합 비용을 작은 단위로 계속 지불한다
배포와 출시를 분리할 수 있다deploy는 자주, release는 통제해서 할 수 있다

7.6 counterexample: CI와 flag 없이 TBD만 흉내 내면

섹션 제목: “7.6 counterexample: CI와 flag 없이 TBD만 흉내 내면”

팀이 “빠르게 가자”며 develop을 없애고 모두 main에 직접 머지한다고 하자. 그런데 CI가 모든 커밋에서 돌지 않고, feature flag도 없다. 그러면 미완성 결제 v2 코드가 main에 들어간 순간 사용자에게 노출된다. 다음 장애는 브랜치 전략 문제가 아니라 검증과 노출 제어가 없는 배포 문제가 된다.

TBD는 branch를 줄이는 전략이 아니라 자동 검증과 노출 제어를 전제로 통합을 자주 하는 전략이다. 전제가 없으면 “짧은 브랜치”가 아니라 “깨진 main”만 남는다.

  • main이 자주 깨지는데 원인 commit을 빨리 찾지 못한다.
  • flag 기본값이 불명확해서 장애 시 OFF가 안전한지 확신할 수 없다.
  • 100% rollout이 끝난 release toggle이 몇 달씩 남아 있다.
  • PR이 작지 않고 “이번 sprint 작업 전체”처럼 큰 묶음으로 열린다.
  • CI가 느려서 개발자가 결과를 기다리지 않고 다음 작업을 시작한다.

8. 병합 방식: merge commit, squash, rebase

섹션 제목: “8. 병합 방식: merge commit, squash, rebase”

브랜치 전략과 병합 방식은 별개지만 서로 강하게 영향을 준다. Git Flow는 --no-ff merge commit으로 기능 경계를 남기는 쪽이 자연스럽다. GitHub Flow는 squash merge로 main 이력을 읽기 쉽게 만드는 경우가 많다. rebase는 개인 브랜치 정리에는 좋지만 공유 브랜치에는 위험하다.

시작 상태:
main: 1──2
feature: ╲──A──B──C
Merge commit:
main: 1──2────────M
╲ ╱
feature: A──B──C
남는 것: A, B, C, M
좋은 점: 기능 묶음 경계와 원본 이력이 남는다.
주의점: 작은 PR이 많으면 main 그래프가 복잡해진다.
Squash merge:
main: 1──2──ABC
남는 것: ABC 하나
좋은 점: main 이력이 PR 단위로 깔끔하다.
주의점: feature branch의 개별 commit 해시가 main에 남지 않는다.
Rebase:
main: 1──2──A'──B'──C'
남는 것: 재작성된 A', B', C'
좋은 점: 선형 이력이 된다.
주의점: commit hash가 바뀌므로 공유 브랜치에 쓰면 다른 사람 이력을 흔든다.
방식쓰기 좋은 경우피해야 할 경우
Merge commitrelease branch, 오래 유지된 브랜치, 감사 추적이 중요한 기능 묶음작은 PR이 매우 많고 main 이력을 PR 단위로만 읽고 싶을 때
Squash mergefeature branch를 병합 후 삭제하고 PR 단위로 히스토리를 보고 싶을 때같은 branch를 계속 재사용하거나 commit 단위 bisect가 중요할 때
Rebase아직 공유하지 않은 개인 branch를 최신 main 위로 정리할 때이미 원격에 공유되어 다른 사람이 기반으로 삼은 branch

8.3 counterexample: squash 후 같은 브랜치를 계속 쓰면

섹션 제목: “8.3 counterexample: squash 후 같은 브랜치를 계속 쓰면”

feature branch에 A, B, C 세 commit이 있고, GitHub에서 squash merge로 ABC 한 commit을 main에 넣었다고 하자. feature branch에는 여전히 A, B, C가 남아 있다. 이후 같은 branch에서 D를 추가하고 다시 PR을 열면 Git은 A/B/CABC를 같은 변경이라고 확신하지 못한다. 이미 들어간 변경이 다시 diff로 보이거나 충돌이 생긴다.

그래서 squash merge를 기본으로 쓰는 팀은 병합된 feature branch를 삭제하고 새 작업은 최신 main에서 새 branch로 시작한다. 이것은 취향이 아니라 Git 이력 모델의 결과다.


브랜치 전략 선택 요약

Git Flow

main, develop, feature, release, hotfix를 분리해 버전 릴리즈와 긴급 패치를 명확히 관리한다.

모바일 앱, SDK, 설치형 제품, 장기 지원 브랜치처럼 여러 버전을 동시에 지원할 때.

GitHub Flow

main과 짧은 feature branch, PR 리뷰, status check, branch protection으로 운영한다.

SaaS나 웹서비스에서 PR 완료마다 배포하고 싶은 소~중규모 팀의 기본값.

Trunk-Based Development

trunk에 자주 통합하고 미완성 기능은 feature flag로 숨긴다.

자동화 테스트와 flag 인프라가 충분하고 배포 빈도를 극대화해야 할 때.
브랜치 전략 의사결정 흐름
flowchart TD
Start["전략 선택"] --> MultiVersion{"여러 버전 동시 지원?"}
MultiVersion -->|yes| GitFlow["Git Flow"]
MultiVersion -->|no| ReleaseTrain{"릴리즈 기차가 필요한가?"}
ReleaseTrain -->|yes| GitFlow
ReleaseTrain -->|no| MainDeploy{"main을 항상 배포 가능하게 유지할 수 있는가?"}
MainDeploy -->|no| GitHubFlow["GitHub Flow부터 정비"]
MainDeploy -->|yes| Automation{"빠른 CI와 feature flag가 충분한가?"}
Automation -->|yes| TBD["Trunk-Based Development"]
Automation -->|no| GitHubFlow
GitHubFlow --> Guard["PR + status check + branch protection"]
TBD --> Flag["짧은 branch + flag 정리 루틴"]

9.1 판단 기준을 질문으로 바꾸기

섹션 제목: “9.1 판단 기준을 질문으로 바꾸기”
질문예라면아니라면
여러 버전을 동시에 지원하는가?Git Flow 후보GitHub Flow/TBD 후보
main이 언제든 배포 가능해야 하는가?GitHub Flow/TBD 후보Git Flow나 release branch 필요
CI 결과를 10분 안팎으로 받을 수 있는가?짧은 PR 유지 가능TBD 전환은 이르다
미완성 기능을 flag로 안전하게 숨길 수 있는가?TBD 후보GitHub Flow 기본값이 안전
hotfix가 다음 정규 릴리즈에도 반드시 들어가야 하는가?Git Flow의 되돌림 규칙 필요단순 PR hotfix로 충분할 수 있다
PR이 평균 2일 넘게 열려 있는가?long-lived branch 위험짧은 통합 리듬 유지 중

9.2 BackOps 같은 Nest.js + AWS 서비스라면

섹션 제목: “9.2 BackOps 같은 Nest.js + AWS 서비스라면”

개발자 4명, ECS 배포 주 3~5회, 구버전 장기 지원이 없는 운영 도구를 가정하면 기본값은 GitHub Flow가 현실적이다. main 보호 규칙, PR squash merge, staging 자동 배포, production 수동 승인 정도로 시작하면 브랜치 모델은 단순하고 품질 경계는 분명하다.

이후 다음 신호가 쌓이면 TBD로 좁힐 수 있다.

  • PR 평균 수명이 1일 안쪽이다.
  • CI가 핵심 테스트를 빠르게 돌리고 실패 원인을 신뢰할 수 있다.
  • AWS AppConfig, LaunchDarkly, 자체 flag 시스템 등으로 신규 기능을 OFF 배포할 수 있다.
  • release toggle 정리 주인이 정해져 있고, 100% rollout 후 삭제 PR이 자연스럽다.

반대로 고객사별 설치형 버전, 장기 지원 브랜치, 월간 QA 릴리즈가 생기면 Git Flow의 release/*hotfix/* 분리가 다시 비용보다 이득이 커질 수 있다.


10. Worked examples: 작은 상황으로 전략을 읽기

섹션 제목: “10. Worked examples: 작은 상황으로 전략을 읽기”

브랜치 전략은 표로만 보면 “팀 취향”처럼 보인다. 실제 판단은 작은 상황을 놓고 어디에서 위험을 흡수할지 보는 방식이 더 좋다.

상품 검색 기능을 추가한다고 하자. 필요한 변경은 API endpoint, 검색 service, 인덱스 쿼리, 테스트 네 가지다.

전략작업 흐름위험을 흡수하는 지점
Git Flowdevelop에서 feature/search 생성 -> develop 병합 -> 다음 release/*에 포함release branch에서 QA와 버그 픽스를 흡수한다
GitHub Flowmain에서 feature/search 생성 -> PR -> CI/status check -> main 병합PR과 branch protection에서 검증을 흡수한다
TBD작은 commit/PR을 trunk에 자주 합침 -> new-search flag OFF -> 점진 rolloutCI와 feature flag가 통합/노출 위험을 나눠 흡수한다

같은 기능이어도 위험의 위치가 다르다. Git Flow는 “이번 릴리즈에 포함할지”가 중요한 질문이다. GitHub Flow는 “이 PR이 main을 깨지 않는지”가 중요하다. TBD는 “이 코드를 지금 trunk에 넣어도 사용자에게 노출되지 않는지”가 중요하다.

10.2 수치 감각: PR 크기와 리뷰 신뢰

섹션 제목: “10.2 수치 감각: PR 크기와 리뷰 신뢰”

PR 크기에 절대 정답은 없지만, 리뷰 신뢰도는 변경량과 범위가 커질수록 빠르게 떨어진다. 40줄 변경은 리뷰어가 의도와 부작용을 머릿속에 유지하기 쉽다. 400줄 변경은 테스트가 있어도 “무엇이 위험한지”를 놓치기 쉽다. 2000줄 변경은 리뷰가 승인 절차로 변할 가능성이 크다.

다음 표는 규칙이 아니라 읽는 감각이다.

PR 상태리뷰어가 볼 수 있는 것위험
1개 모듈, 50줄 안팎의도, edge case, 테스트 누락낮음
3개 모듈, 300줄 안팎설계 의도는 보이나 세부 부작용은 놓칠 수 있음중간
여러 계층, 1000줄 이상리뷰어가 흐름을 재구성해야 함높음
migration + business logic + 배포 설정 동시 변경실패 원인이 섞임매우 높음

GitHub Flow나 TBD에서 큰 PR이 반복되면 전략은 짧은 branch를 쓰는 것처럼 보여도 실제로는 long-lived integration을 하고 있는 셈이다. 반대로 Git Flow에서도 feature branch를 작은 PR 단위로 develop에 자주 합치면 통합 위험이 줄어든다.

10.3 반례: release branch가 안정화가 아니라 주차장이 될 때

섹션 제목: “10.3 반례: release branch가 안정화가 아니라 주차장이 될 때”

Git Flow 팀이 release/2.0.0을 만들었다. 원래 목적은 QA 중 발견된 버그만 고치는 것이었다. 그런데 배포가 밀리자 팀은 “이미 release branch가 있으니 여기에 넣자”며 새 기능을 계속 추가한다.

release/2.0.0 시작: 검색 v2, 결제 안정화
3일 뒤: 관리자 통계 기능 추가
5일 뒤: 로그인 UI 변경 추가
8일 뒤: 검색 v2 버그 픽스와 통계 기능 버그가 섞임

이 순간 release branch는 안정화 브랜치가 아니라 두 번째 develop이 된다. Git Flow의 장점인 “이번 릴리즈 범위 고정”이 사라지고, release와 develop의 차이는 계속 커진다. 해결책은 명령어가 아니라 scope discipline이다. release branch가 잘린 뒤에는 새 기능을 다음 release로 미루고, 현재 release에는 blocker와 regression만 넣는다.

10.4 반례: branch protection이 있는데도 main이 깨지는 경우

섹션 제목: “10.4 반례: branch protection이 있는데도 main이 깨지는 경우”

branch protection이 있다고 해서 main이 자동으로 안전해지는 것은 아니다. 다음 상황을 보자.

  1. required status check는 ci/test 하나뿐이다.
  2. typecheck와 lint는 로컬에서만 돌린다.
  3. PR A가 UserDto 필드명을 바꾸고 ci/test를 통과한다.
  4. PR B가 같은 DTO를 쓰는 새 endpoint를 추가하고 ci/test를 통과한다.
  5. strict mode가 꺼져 있어 PR B가 최신 main 위에서 재검증되지 않는다.
  6. main 병합 뒤 type error가 배포 단계에서 발견된다.

겉으로는 “보호 규칙이 있었다”. 실제로는 필요한 검증이 status check에 포함되지 않았고, 최신 조합 검증도 없었다. branch protection의 품질은 체크박스 수가 아니라 “어떤 실패를 병합 전에 잡는가”로 판단해야 한다.

10.5 Feature flag lifecycle worked example

섹션 제목: “10.5 Feature flag lifecycle worked example”

TBD에서 new-search release toggle을 만든다고 하자. 좋은 flag는 생성, rollout, 삭제가 한 흐름으로 설계된다.

시점상태코드/운영 의미
Day 0new-search 기본 OFF미완성 코드가 main에 들어가도 사용자에게 보이지 않는다
Day 3내부 사용자 1%오류율, latency, 검색 품질을 작은 범위에서 본다
Day 750%기존 경로와 새 경로를 비교한다
Day 10100%fallback이 필요한지 마지막으로 확인한다
Day 14flag와 legacy path 삭제release toggle을 제품 코드에서 제거한다

삭제가 빠지면 Day 14 이후에도 두 코드 경로가 남는다. 다음 기능이 또 flag를 추가하면 경로 조합이 늘어난다. flag는 TBD를 가능하게 하는 장치이지만, cleanup이 없으면 장기 branch를 코드 안으로 옮겨놓은 것과 비슷해진다.

10.6 운영 지표로 보는 전환 판단

섹션 제목: “10.6 운영 지표로 보는 전환 판단”

전략 전환은 “우리도 TBD 하자” 같은 선언으로 시작하면 위험하다. 현재 지표를 보고 어떤 병목을 줄일지 정해야 한다.

현재 관찰해석전환 방향
PR 평균 수명 4일, 충돌 PR 20%branch가 오래 살아 통합 비용이 크다PR 쪼개기와 GitHub Flow 정비가 먼저
main 실패가 주 3회 이상main 품질 경계가 약하다branch protection/status check 강화
release branch가 매번 2주 이상 유지release scope가 크거나 QA 병목이 있다release scope 축소 또는 배포 자동화 개선
배포는 자주 하지만 기능 노출은 조심스럽다deploy와 release 분리가 필요하다feature flag 도입 후 TBD 일부 적용
hotfix가 다음 버전에서 재발장기 브랜치 간 되돌림 누락Git Flow hotfix merge 규칙 강화

이 지표들은 외부 보고서의 숫자를 그대로 맞추기 위한 것이 아니다. 팀이 통합 비용을 어디서 내고 있는지 찾기 위한 관찰 도구다.

10.7 선택 기준을 한 문장으로 압축하기

섹션 제목: “10.7 선택 기준을 한 문장으로 압축하기”
  • Git Flow: “여러 버전과 릴리즈 기차가 실제 제품 요구사항이면 쓴다.”
  • GitHub Flow: “main을 배포 가능하게 지키는 PR 중심 기본값이 필요하면 쓴다.”
  • TBD: “작은 변경, 빠른 CI, feature flag cleanup으로 통합을 매일 흡수할 수 있으면 쓴다.”

반대로 피해야 하는 조건도 한 문장으로 외울 수 있다.

  • Git Flow를 피할 조건: 릴리즈 브랜치가 안정화가 아니라 기능 주차장이 된다.
  • GitHub Flow를 피할 조건: main이 자주 깨지고 status check가 우회된다.
  • TBD를 피할 조건: CI와 flag 없이 branch만 없애려 한다.

11. 실패 신호: 증상 -> 개념 -> 이유 -> 첫 확인

섹션 제목: “11. 실패 신호: 증상 -> 개념 -> 이유 -> 첫 확인”
증상연결되는 개념왜 문제인가첫 확인
PR마다 충돌이 크고 리뷰가 늦어진다long-lived feature branch통합 비용을 뒤로 미루고 있다branch 나이와 PR open 시간을 본다
main CI가 늘 실패한다main always deployable 붕괴실패가 정상 상태가 되어 status check 신뢰가 사라진다최근 main 실패 commit과 bypass merge를 본다
hotfix가 다음 release에서 사라진다Git Flow 이중 장기 브랜치main 패치가 develop에 되돌아가지 않았다hotfix merge 대상이 main/develop 둘 다인지 본다
green check였는데 병합 후 main이 깨진다strict status / merge queue 부재낡은 base에서 검증한 PR이 최신 조합에서 실패했다PR base sha와 병합 직전 재검증 여부를 본다
squash merge 후 같은 branch에서 충돌이 반복된다squash와 branch 재사용ABCA/B/C를 Git이 같은 이력으로 보지 않는다병합 후 branch 삭제 여부를 본다
flag가 너무 많아 테스트 조합을 모른다release toggle cleanup 부채코드 경로가 조합 폭발을 일으킨다100% rollout flag와 owner/removal date를 본다
TBD 도입 후 main이 자주 깨진다전제 없는 TBDCI/flag/test 없이 branch만 줄였다모든 commit 검증과 flag 기본값을 본다

이 표의 목적은 즉시 명령어를 외우는 것이 아니다. 증상을 보면 어떤 브랜치 전략 원리가 깨졌는지 먼저 연결하는 것이다. 명령어는 그 다음이다.


12.1 “브랜치가 많으면 안전하다”

섹션 제목: “12.1 “브랜치가 많으면 안전하다””

브랜치가 많으면 미완성 변경이 보이지 않는 곳에 숨을 뿐이다. 안전은 브랜치 수가 아니라 검증 경계, 통합 주기, 릴리즈 경계에서 나온다. feature/big-refactor가 3주 동안 열려 있으면 main은 조용해 보이지만, 병합 시점에 충돌과 회귀가 한꺼번에 드러난다.

12.2 “GitHub Flow는 규칙이 적으니 느슨하다”

섹션 제목: “12.2 “GitHub Flow는 규칙이 적으니 느슨하다””

GitHub Flow는 브랜치 종류가 적을 뿐 규율은 강해야 한다. main이 항상 배포 가능해야 하고, PR과 status check가 병합 조건이어야 한다. branch protection 없이 운영되는 GitHub Flow는 단순한 전략이 아니라 무규칙 feature branch에 가깝다.

12.3 “TBD는 main에 직접 push하는 것”

섹션 제목: “12.3 “TBD는 main에 직접 push하는 것””

TBD는 main에 아무거나 넣는 방식이 아니다. 작은 변경, 빠른 CI, feature flag, code review 또는 pair review, 빠른 rollback이 모두 필요하다. trunk가 자주 깨지면 TBD의 장점인 빠른 통합이 아니라 팀 전체의 대기 시간이 늘어난다.

12.4 “feature flag를 쓰면 미완성 코드를 마음껏 넣어도 된다”

섹션 제목: “12.4 “feature flag를 쓰면 미완성 코드를 마음껏 넣어도 된다””

feature flag는 위험을 없애지 않고 위험의 종류를 바꾼다. 브랜치 충돌 위험은 줄지만, flag 조합과 cleanup 부채가 생긴다. flag OFF가 안전한 기본값인지, flag 서비스 장애 시 어떤 경로로 가는지, 100% rollout 뒤 누가 삭제하는지까지 설계해야 한다.

12.5 “rebase는 항상 이력을 깨끗하게 한다”

섹션 제목: “12.5 “rebase는 항상 이력을 깨끗하게 한다””

rebase는 commit을 새 base 위에 다시 쓰는 작업이다. 혼자 쓰는 branch에서는 이력을 정리할 수 있지만, 이미 원격에 push했고 다른 사람이 그 branch를 기반으로 작업했다면 rebase는 다른 사람의 commit 기준점을 흔든다. 공유 branch에는 merge나 새 branch 전략이 더 안전하다.


13. 브랜치 네이밍은 전략의 본체가 아니다

섹션 제목: “13. 브랜치 네이밍은 전략의 본체가 아니다”

브랜치 이름은 필요하지만 전략의 본체는 아니다. 이름이 좋아도 PR이 크고 CI가 느리고 main이 깨져 있으면 전략은 실패한다.

feature/TICKET-123-user-auth
fix/TICKET-456-payment-null
chore/update-node-version
release/v1.4.0
hotfix/payment-crash

좋은 이름은 다음 정보를 짧게 담는다.

  • 변경 종류: feature, fix, chore, release, hotfix
  • 추적 단위: 티켓 번호나 이슈 번호
  • 변경 의도: user-auth, payment-null처럼 검색 가능한 단어

이름에서 피해야 할 것은 사람 이름만 있는 branch, 범위가 너무 큰 feature/sprint-12, 끝난 뒤에도 남아 있는 test2 같은 branch다. 이런 이름은 충돌이 생겼을 때 소유자와 의도를 찾기 어렵게 만든다.


14. 선택 기준: 팀 상황별 권장 시작점

섹션 제목: “14. 선택 기준: 팀 상황별 권장 시작점”
팀/제품 상황시작 전략이유다음 전환 신호
개인 프로젝트, 작은 웹 서비스GitHub FlowPR과 CI만으로 충분히 단순하다PR이 매우 작고 flag가 갖춰지면 TBD 일부 도입
소규모 SaaS 팀, 배포 주 1~5회GitHub Flowmain 보호와 staging 배포로 품질 경계를 만들 수 있다배포 빈도를 더 올리고 싶고 flag cleanup이 가능할 때 TBD
모바일 앱, SDK, 설치형 제품Git Flow릴리즈 후보와 hotfix 경계가 필요하다서버 사이드 컴포넌트는 GitHub Flow로 분리 가능
대규모 monorepo, 빠른 자동화 테스트TBD긴 branch보다 작은 통합을 자주 흡수하는 편이 낫다flag 부채나 CI 지연이 늘면 branch discipline 재점검
테스트가 약하고 release도 불규칙한 팀GitHub Flow 정비부터TBD/Git Flow보다 main 보호와 CI 신뢰 회복이 먼저다main 안정화 후 release 또는 TBD 선택

전략은 한 번 고르면 영원히 고정되는 아키텍처가 아니다. 제품의 릴리즈 모델과 팀의 자동화 수준이 바뀌면 브랜치 전략도 바뀐다. 중요한 것은 전략을 바꿀 때 어떤 실패 비용을 줄이고 어떤 새 비용을 받아들이는지 명시하는 것이다.

14.1 Git Flow에서 GitHub Flow로 줄일 때

섹션 제목: “14.1 Git Flow에서 GitHub Flow로 줄일 때”

Git Flow가 무겁게 느껴진다고 바로 TBD로 가면 실패하기 쉽다. 먼저 release/*develop이 실제로 어떤 가치를 주는지 확인한다.

관찰의미조치
release branch에서 버그 픽스보다 새 기능 추가가 많다release branch가 안정화 경계로 작동하지 않는다release branch 제거 후보
develop이 자주 깨져 feature branch 시작점으로 불안하다develop이 품질 경계가 아니라 임시 적재소가 됐다PR을 main 기준으로 단순화
운영 hotfix가 드물고 대부분 PR로 처리 가능하다Git Flow의 hotfix 복잡도가 과할 수 있다GitHub Flow + tag/release note로 대체
배포가 주 3회 이상이고 QA가 자동화되어 있다릴리즈 기차보다 continuous delivery에 가깝다main 보호 규칙 강화 후 GitHub Flow

전환은 브랜치 삭제가 아니라 품질 경계 이동이다. develop을 없앤다면 main에 required status check, 리뷰 승인, 배포 환경 승인 같은 경계가 대신 있어야 한다.

GitHub Flow에서 TBD로 가는 핵심은 PR을 없애는 것이 아니라 PR과 branch를 더 작게 만드는 것이다. 많은 팀은 완전한 direct-to-trunk보다 “짧은 PR + feature flag” 형태로 시작한다.

작은 전환 순서는 다음과 같다.

  1. PR 크기를 줄인다. 하나의 PR이 한 가지 기능 의도만 갖게 한다.
  2. CI 시간을 줄인다. 오래 걸리는 e2e는 nightly로만 보내지 말고 핵심 smoke test를 PR 게이트에 남긴다.
  3. release toggle을 도입한다. 기본값 OFF, owner, 제거 목표일을 함께 기록한다.
  4. main 배포와 기능 release를 분리한다. 배포는 자동화하되 사용자 노출은 flag로 조절한다.
  5. 오래 열린 branch를 측정한다. 평균 수명과 p90 수명을 같이 본다.

여기서 p90은 전체 branch 중 90%가 이 시간 안에 병합된다는 뜻이다. 평균이 1일이어도 p90이 7일이면 일부 큰 branch가 계속 통합 위험을 만들고 있다는 신호다.

브랜치 전략은 감으로만 운영하면 “우리 팀은 빠르다”와 “우리 팀은 안정적이다”가 동시에 주장된다. 최소한 다음 지표는 한 달 단위로 볼 만하다.

지표왜 보는가위험 신호
PR open -> merge 평균/p90branch 수명과 리뷰 병목을 본다p90이 계속 5일 이상
PR당 변경 파일 수리뷰 가능한 범위인지 본다10개 파일 이상 PR이 반복
main 실패 횟수기본 브랜치 신뢰도를 본다실패가 정상 상태처럼 방치
merge conflict 발생 PR 비율통합 지연 비용을 본다같은 모듈에서 반복 충돌
revert/rollback 횟수검증 경계가 충분한지 본다특정 유형 변경에서 반복
flag agerelease toggle cleanup을 본다100% rollout 후 2주 이상 잔존

이 지표의 목적은 팀을 감시하는 것이 아니다. 전략이 약속한 비용 구조가 실제로 작동하는지 확인하는 것이다. GitHub Flow를 쓰는데 PR p90이 10일이면 “짧은 feature branch”라는 약속이 깨진다. TBD를 쓰는데 flag age가 계속 늘면 “브랜치 대신 flag가 장기 분기점이 되는” 실패가 시작된다.

14.4 전략 선택에서 자주 빠지는 비용

섹션 제목: “14.4 전략 선택에서 자주 빠지는 비용”

브랜치 전략을 고를 때는 눈에 보이는 명령어보다 숨은 비용을 봐야 한다.

숨은 비용Git FlowGitHub FlowTBD
교육 비용브랜치 역할이 많아 높다낮다원리는 단순하지만 실천 난도가 높다
CI 비용release/develop/main 여러 경계에서 돈다PR/main 중심매우 자주 돌아야 한다
리뷰 비용feature/release 경계가 크면 커진다PR 크기에 직접 비례작은 리뷰를 자주 해야 한다
운영 비용hotfix 되돌림과 release tag 관리branch protection/merge queue 관리flag lifecycle과 빠른 rollback 관리
인지 비용현재 변경이 어느 브랜치에 들어가야 하는지 판단main 기준으로 단순deploy/release/flag 상태를 함께 봐야 함

이 표를 보면 “단순한 전략”도 공짜가 아니라는 점이 보인다. GitHub Flow는 branch 역할은 단순하지만 branch protection을 정확히 걸어야 한다. TBD는 branch 구조는 가장 단순하지만 feature flag와 CI 운영 비용이 크다. Git Flow는 도구와 규칙이 많지만 여러 버전 지원 비용을 명시적으로 감당한다.

14.5 전략 변경 메모에 들어갈 질문

섹션 제목: “14.5 전략 변경 메모에 들어갈 질문”

브랜치 전략을 바꿀 때는 결정 기록을 짧게 남기면 좋다. 다음 질문에 답하지 못하면 아직 전략 문제가 아니라 팀 운영 문제가 정리되지 않은 상태일 수 있다.

질문답해야 하는 이유
지금 가장 큰 비용은 충돌, 리뷰 지연, 릴리즈 지연, rollback 중 무엇인가?바꾸려는 전략이 실제 병목을 줄이는지 확인한다
새 전략에서 main을 누가 어떻게 보호하는가?품질 경계가 사라지지 않게 한다
미완성 기능은 branch, release branch, feature flag 중 어디에 머무르는가?격리 위치를 명확히 한다
긴급 패치는 다음 정규 릴리즈로 어떻게 되돌아가는가?hotfix 재발을 막는다
한 달 뒤 어떤 지표가 좋아져야 성공인가?전략 변경을 취향 논쟁이 아니라 관찰 가능한 실험으로 만든다

이 절은 브랜치 전략 본문을 이해한 뒤 그래프를 눈으로 확인하기 위한 부록이다. 긴 설치 runbook보다 “무엇을 관찰해야 하는가”에 집중한다.

15.1 merge commit과 squash merge 그래프 보기

섹션 제목: “15.1 merge commit과 squash merge 그래프 보기”
Terminal window
git init merge-practice
cd merge-practice
git commit --allow-empty -m "initial"
git checkout -b feature/a
git commit --allow-empty -m "feat: A 1"
git commit --allow-empty -m "feat: A 2"
git checkout main
git merge --no-ff feature/a -m "Merge feature/a"
git log --oneline --graph --all

관찰할 것:

  • merge commit이 하나 생겼는가?
  • feature/a의 두 commit이 그래프 옆 가지로 보이는가?
  • git revert <merge-commit>을 하면 기능 묶음 단위 revert가 가능하다는 점을 이해했는가?

Squash도 같은 저장소에서 새 branch로 확인할 수 있다.

Terminal window
git checkout -b feature/b main
git commit --allow-empty -m "feat: B 1"
git commit --allow-empty -m "feat: B 2"
git checkout main
git merge --squash feature/b
git commit -m "feat: B"
git log --oneline --graph --all

관찰할 것:

  • main에는 feat: B 하나만 남는가?
  • feature/b에는 원래 commit 두 개가 여전히 남아 있는가?
  • squash 후 같은 branch를 계속 쓰면 왜 이력이 어긋날 수 있는지 설명할 수 있는가?

TBD나 GitHub Flow에서는 branch 수명 자체가 품질 신호다.

Terminal window
git for-each-ref \
--format="%(refname:short) %(committerdate:relative)" \
refs/heads

관찰할 것:

  • 며칠 이상 오래된 feature branch가 많은가?
  • 오래된 branch가 같은 파일을 계속 건드리는가?
  • 오래된 branch가 많다면 작업을 더 작게 자를 수 있는가, 아니면 release branch가 필요한 제품 상황인가?

15.3 branch protection은 UI/API보다 조건을 먼저 본다

섹션 제목: “15.3 branch protection은 UI/API보다 조건을 먼저 본다”

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,
strict: .required_status_checks.strict,
enforce_admins: .enforce_admins.enabled
}'

관찰할 것:

  • required status check가 실제 CI job 이름과 맞는가?
  • strict가 꺼져 있다면 merge queue나 update branch 습관이 있는가?
  • 관리자 우회가 자주 쓰이는가?

15.4 hotfix 되돌림을 손으로 추적하기

섹션 제목: “15.4 hotfix 되돌림을 손으로 추적하기”

Git Flow를 쓴다면 hotfix가 두 장기 브랜치에 모두 들어갔는지 확인하는 습관이 중요하다.

Terminal window
git log --oneline main --grep "payment-null"
git log --oneline develop --grep "payment-null"

관찰할 것:

  • hotfix commit 또는 equivalent merge가 maindevelop 양쪽에 있는가?
  • release branch가 이미 존재한다면 release branch에도 반영되었는가?
  • 다음 release note에 같은 장애가 다시 등장하지 않게 추적 단위가 남았는가?

Git Flow 도입 기준

  • `main`과 `develop` 브랜치의 역할이 분리되어 있다.
  • feature 브랜치는 `develop`에서 파생하고 병합 후 삭제한다.
  • release 브랜치는 새 기능 유입을 막고 안정화 변경만 받는다.
  • hotfix 브랜치는 `main`뿐 아니라 `develop`과 필요 시 release branch에도 반영한다.
  • merge commit 또는 tag로 릴리즈와 기능 묶음 경계를 추적할 수 있다.

GitHub Flow 도입 기준

  • `main` 브랜치가 항상 배포 가능한 상태다.
  • 모든 변경사항이 PR을 통해 병합된다.
  • required status check와 최소 리뷰 승인이 branch protection으로 강제된다.
  • strict status check 또는 merge queue로 낡은 green check 문제를 줄인다.
  • squash merge 후 feature branch를 삭제하고 새 작업은 최신 `main`에서 시작한다.

TBD 도입 기준

  • 브랜치 수명을 대체로 1~2일 안쪽으로 관리하려는 리듬이 있다.
  • CI가 모든 commit 또는 PR에서 빠르게 실행되고 결과를 신뢰할 수 있다.
  • feature flag로 코드 배포와 사용자 노출을 분리할 수 있다.
  • release toggle의 owner와 제거 목표일이 관리된다.
  • main이 깨졌을 때 원인 commit을 빠르게 찾고 되돌릴 수 있다.

공통 운영 기준

  • 팀이 merge commit, squash merge, rebase 중 기본 병합 방식을 합의했다.
  • 브랜치 이름이 변경 종류와 추적 단위를 드러낸다.
  • PR 크기와 수명 지표를 주기적으로 본다.
  • 브랜치 전략이 제품 릴리즈 모델과 맞는지 분기마다 점검한다.

퀴즈

Git Flow에서 hotfix를 main에만 병합하고 develop에 반영하지 않으면 어떤 문제가 생기는가?

힌트: Git Flow는 main과 develop이라는 두 장기 브랜치를 동시에 유지한다.

정답 보기

다음 release가 develop에서 만들어질 때 hotfix가 빠져 같은 버그가 다시 프로덕션에 들어갈 수 있다.


키워드설명
main / master제품 기준이 되는 기본 브랜치. 최근 저장소는 보통 main을 쓴다
developGit Flow에서 다음 릴리즈 후보 기능이 모이는 통합 개발 브랜치
feature branch기능/수정 단위의 임시 작업 브랜치
release branch배포 후보를 안정화하는 임시 브랜치
hotfix branch프로덕션 긴급 패치를 위한 브랜치
trunkTBD에서 모두가 자주 합류하는 중심 브랜치
Pull Request(PR)변경 단위, 리뷰 단위, 자동 검증 단위
status checkCI 빌드, 테스트, 린트 같은 자동 검증 결과
branch protection특정 브랜치에 병합/푸시 조건을 강제하는 규칙
merge queue최신 base 조합에서 PR을 순차 검증한 뒤 병합하는 큐
feature flag코드 배포와 기능 노출을 분리하는 스위치
release toggle출시 전 기능을 숨기는 임시 flag
merge commit두 브랜치 이력을 합친 경계를 남기는 commit
squash mergePR commit 여러 개를 하나로 압축해 병합하는 방식
rebasecommit을 다른 base 위에 다시 쌓아 이력을 재작성하는 방식
DORADevOps 성과를 배포 빈도, 리드타임, 변경 실패율, 복구 시간 등으로 보는 연구/측정 프레임


브랜치 전략은 naming convention이 아니라 충돌, 리뷰, 릴리즈, 롤백 경계를 설계하는 방식이다.

Git Flow는 maindevelop, release, hotfix를 나눠 여러 버전과 릴리즈 기차를 관리한다. 장점은 릴리즈 경계와 긴급 패치 추적성이고, 비용은 긴 branch와 복잡한 통합 흐름이다.

GitHub Flow는 짧은 feature branch와 PR, status check, branch protection으로 main을 지킨다. 기본값으로 쓰기 좋지만 main이 항상 배포 가능하다는 규율이 무너지면 전략 전체가 무너진다.

TBD는 trunk에 자주 통합하고 feature flag로 deploy와 release를 분리한다. 빠른 CI, 작은 변경, flag cleanup이 없으면 단순히 branch만 줄인 위험한 운영이 된다.

병합 방식도 전략의 일부처럼 다뤄야 한다. merge commit은 경계를 남기고, squash는 main을 깔끔하게 만들며, rebase는 이력을 재작성한다. 어떤 방식을 쓰든 “병합 후 branch를 어떻게 다룰지”까지 팀 규칙에 포함해야 한다.