On this page

이 글은 책 『팀 개발을 위한 Git, GitHub 시작하기』를 읽으며 정리한 내용을 블로그용으로 다시 다듬은 글입니다. 앞의 글에서 커밋, HEAD, 브랜치 포인터, merge/rebase/reset/revert를 봤다면, 이번 글에서는 그 브랜치를 팀에서 어떤 규칙으로 굴릴지 정리합니다.

Git에 대해 배웠고 브랜치가 무엇인지도 알았다면 이제 팀에서 브랜치를 어떻게 쓸지 정해야 한다. 이를 쉽게 브랜치 전략이라고 부른다.

그렇다면 이게 왜 필요한 걸까? 바로 아래와 같은 질문들에 답하기 위해서라고 할 수 있다.

어디에 직접 커밋해도 되나?
언제 PR을 보내나?
어느 브랜치가 배포되는 코드인가?
긴급 버그는 어디서 고치나?

다시 말해 브랜치 전략은 팀의 협업 방식과 배포 방식을 Git 브랜치로 표현한 것이라고 볼 수 있다.

먼저 브랜치의 책임을 정해야 한다

책에서 소개된 일반적인 브랜치 전략은 이렇게 잡아볼 수 있다.

  1. feat/기능이름: 각 개발자가 개발 중인 브랜치. 직접 커밋을 올림.
  2. main: feat/기능이름 브랜치에서 개발 완료된 코드가 합쳐진 브랜치. 출시 전 베타 버전. 직접 커밋하지 않고 병합으로만 업데이트.
  3. latest: 실제 출시할 코드, 즉 대중에게 보여줄 완벽한 코드를 올리는 브랜치.

master라는 이름도 쓰지만 요즘은 보통 main을 많이 쓴다고 한다. 이름은 팀 규칙에 맞추면 된다. 핵심은 역할이다.

feature/login  -> 기능 개발
main           -> 통합된 코드
latest         -> 실제 배포 코드

여기서 중요한 규칙은 이것이다.

배포되는 브랜치에는 아무나 직접 커밋하지 않는다.

기능 브랜치에서 작업하고, PR을 만들고, 리뷰를 받고, 테스트를 통과한 뒤에 병합한다.

Feature Branch Workflow: 기능마다 브랜치를 만든다

가장 이해하기 쉬운 방식은 Feature Branch Workflow다.

새 기능을 만들 때마다 별도의 브랜치를 만든다.

git checkout main
git pull
git checkout -b feature/login
# 로그인 기능 개발 시작

작업이 끝나면 main에 바로 합치지 않고 PR을 보낸다.

feature/login -> Pull Request -> main

이 방식의 장점은 단순하다는 것이다.

  • 기능 단위로 작업이 분리된다.
  • PR에서 변경사항을 확인하기 쉽다.
  • 문제가 생기면 해당 기능 브랜치만 보면 된다.
  • 작은 팀에서 시작하기 좋다.

브랜치 이름은 보통 feature/기능이름 형식으로 잡는다. /는 폴더처럼 구분해주는 역할을 한다.

feature/login
feature/payment
bugfix/signup-error
hotfix/prod-500

브랜치 이름은 대충 지어도 Git은 돌아간다. 하지만 test, test2, final, real-final 같은 브랜치가 쌓이면 나중에 본인이 봐도 모른다. 미래의 나에게 너무 큰 숙제를 주지 말자.

Feature Branch Workflow는 이런 팀에 잘 맞는다.

  • 팀 규모가 작거나 중간 정도다.
  • 기능 단위로 PR 리뷰를 하고 싶다.
  • 배포 브랜치가 하나이고, 릴리즈 방식이 복잡하지 않다.
  • Git 전략을 처음 정하는 팀이다.

Pull Request는 합치기 전에 확인하는 과정이다

브랜치 전략에서 PR은 거의 빠지지 않는다.

풀 리퀘스트의 의의는 이렇게 잡을 수 있다.

충돌만 없다고 무작정 합치기보단, 내가 무엇을 바꾸었는지 협력자가 확인하는 과정을 거치는 것이 좋음. 사수 개발자의 허락을 맡는 것.

이 표현은 꽤 이해하기 쉬웠다. PR은 “제가 바꿨으니 확인해주세요”라는 요청이다.

PR을 만들 때 봐야 하는 기본 개념은 basecompare다.

  • base branch: 병합 결과물이 올라갈 브랜치
  • compare branch: 비교 대상이 되는 내 작업 브랜치
base: main
compare: feature/login

이 말은 feature/login의 변경사항을 main에 반영하고 싶다는 뜻이다.

base를 잘못 고르면 엉뚱한 브랜치로 PR을 보내게 된다. GitHub 화면에서 한 번 더 확인하자.

PR에는 제목과 설명도 필요하다.

  • 무엇을 바꿨는지
  • 왜 바꿨는지
  • 테스트는 어떻게 했는지
  • 화면이 있다면 스크린샷
  • 리뷰어가 특히 봐야 하는 부분

PR을 받은 사람은 보통 이런 선택을 한다.

  1. approve: 괜찮으니 병합 가능
  2. request changes: 수정 필요
  3. comment: 질문이나 의견만 남김
  4. merge pull request: 실제 병합

GitHub에서 Merge pull request를 누르면 결국 Git 병합이 일어나는 것이다. GUI에서 하든 CLI에서 하든 원리는 같다.

Gitflow Workflow: 브랜치가 많아지는 대신 역할이 분명하다

Gitflow는 브랜치 종류를 더 세분화한다.

대표적으로 이런 브랜치를 둔다.

  • main: 공식 배포 이력. 버전 태그가 붙는 브랜치.
  • develop: 다음 배포를 위해 개발 중인 최신 코드.
  • feature/*: 새 기능 개발.
  • release/*: 배포 준비.
  • hotfix/*: 운영 중 긴급 버그 수정.

그림으로는 대략 이런 느낌이다.

main     A-----------R--------H
          \         /        /
develop    B---C---D---E----F
             \     /
feature       X---Y

Gitflow의 장점은 역할이 분명하다는 것이다.

main에는 공식 배포 커밋만 남긴다. develop에서는 다음 배포 후보들이 모인다. 새 기능은 feature/*에서 만들고, 배포 준비는 release/*에서 하고, 운영 장애가 나면 hotfix/*로 바로 고친다.

“대중에게 보여줄 완벽한 코드”를 latest 같은 브랜치에 두는 방식도 같은 맥락이다. 실제 배포 코드와 개발 중인 코드를 나누려는 것이다.

책과 자료를 기준으로 보면 Gitflow는 이런 팀에 어울린다.

  • 정해진 릴리즈 주기가 있다.
  • 운영 배포와 다음 개발 흐름을 분리해야 한다.
  • hotfix가 자주 필요하다.
  • 버전 태그와 릴리즈 관리가 중요하다.

하지만 단점도 분명하다.

브랜치가 많다. 규칙도 많다. 처음 보면 “이거 다 외워야 하나?” 싶은 느낌이 든다. 팀이 작고 배포가 자주 일어난다면 오히려 무겁게 느껴질 수 있다.

Release와 tag: 배포 지점을 표시한다

릴리즈와 관련하여 tag도 중요하다.

릴리즈 버전명은 보통 v1.0.0 같은 형식으로 쓰고, “메이저 버전 업.마이너 버전 업.유지보수/버그 수정” 기준으로 숫자를 올린다.

예를 들어 이런 식이다.

v1.0.0
v1.1.0
v1.1.1

Git tag는 특정 커밋에 붙이는 꼬리표로, 역시나 포인터 역할을 한다.

git tag -a v1.0.0 -m "v1.0.0 release"
git push origin v1.0.0
# 그냥 git push만 하면 태그까지 자동으로 올라가지 않을 수 있다.

브랜치는 새 커밋이 생기면 앞으로 가는 반면 tag는 보통 특정 릴리즈 지점을 가리키는 고정된 표시로 쓴다.

A---B---C---D main

      v1.0.0

운영에서 문제가 생겼을 때 “어느 커밋을 배포했는지”를 알아야 한다. tag가 없으면 기억과 감에 의존하게 될 것이다. tag가 있으면 “v1.0.0이 배포된 커밋이 뭐였지?”라고 쉽게 확인할 수 있다.

Forking Workflow: 원본 저장소에 바로 못 올릴 때

Forking Workflow는 개발자 각자가 중앙 저장소를 포크해서 자기 저장소에서 작업하는 방식이다.

오픈소스에 기여할 때 자주 만난다.

upstream/original-repo

        PR

my-forked-repo

원본 저장소에 바로 커밋을 올리려면 소유자이거나 권한을 받은 collaborator여야 한다. 그런데 오픈소스 프로젝트에서 모든 기여자에게 직접 커밋 권한을 줄 수는 없다.

그래서 포크를 쓴다.

  1. 원본 저장소를 fork한다.
  2. 내 계정의 저장소로 clone한다.
  3. 브랜치를 만들고 수정한다.
  4. 내 fork 저장소에 push한다.
  5. 원본 저장소로 Pull Request를 보낸다.

이때 PR 화면에서 head repositorybase repository를 잘 봐야 한다.

  • head repository: 내가 포크한 저장소
  • base repository: 원본 저장소

☞ 브랜치와 포크는 다르다. 브랜치는 같은 저장소 안의 포인터이고, 포크는 저장소 자체를 내 계정으로 복사한 것이다.

팀 규모를 기준으로 보면 “일반적으로 5명 이하인 경우 동일한 저장소 내에서 브랜치를 나눠 협력하는 것이 효율적 but 50명 이상인 경우는 말이 달라진다”는 식으로 생각해볼 수 있다.

작은 팀이면 같은 저장소에서 브랜치와 PR로 충분한 경우가 많다. 기여자가 많고 권한을 열어두기 어려운 프로젝트라면 fork 기반 흐름이 더 안전하다.

Trunk Based Development: 짧게 만들고 빨리 합친다

Trunk Based Development는 모든 개발자가 하나의 중심 브랜치, 보통 main 또는 trunk를 기준으로 작업하는 방식이다.

새 기능을 위한 브랜치를 만들 수는 있지만, 오래 끌지 않는다. 짧게 만들고 빨리 합친다.

main  A---B---C---D---E---F
          \   /
feature    X

핵심은 긴 생명주기의 브랜치를 줄이는 것이다.

브랜치가 오래 살아 있으면 main과 점점 멀어진다. 멀어질수록 충돌 가능성이 올라간다. 나중에 합치려면 고통도 같이 커진다.

자료를 기준으로 보면 Trunk Based Development는 이런 팀에 어울린다.

  • 배포를 자주 한다.
  • 작은 단위로 나눠 작업할 수 있다.
  • feature flag 등으로 미완성 기능을 숨길 수 있다.
  • 오래 살아 있는 브랜치를 싫어한다.

그래서 무엇을 선택해야 할까

상황어울리는 전략
작은 팀, 기능 단위 PR 중심Feature Branch Workflow
정해진 릴리즈 주기, 운영/개발 분리 필요Gitflow Workflow
오픈소스, 외부 기여자 많음Forking Workflow
테스트와 배포 확인이 빠르고, 작게 자주 배포Trunk Based Development

이런 질문을 먼저 해봐야겠다고 정리했다.

우리 팀은 배포를 자주 하나?
운영 hotfix가 자주 필요한가?
외부 기여자를 받아야 하나?
PR 리뷰를 어디서 강제할 것인가?
main에 직접 커밋해도 되는 사람은 누구인가?

Takeaway

여기까지 정리를 마치고 기준은 대략 이렇게 잡으면 될 것 같다.

작은 팀이나 개인 프로젝트라면 Feature Branch Workflow부터 시작하면 될 것 같다. main은 안정된 코드, 기능은 feature/*, 수정은 PR. 이 정도만 지켜도 꽤 많은 혼란이 줄어들 것 같다.

운영 배포와 개발 흐름을 분리해야 한다면 Gitflow 계열을 검토한다. 다만 무조건 Gitflow를 고르지는 않는다. 브랜치가 많아질수록 관리 비용도 같이 늘어나기 때문이다.

외부 기여를 받는다면 Forking Workflow를 쓴다. 권한 관리를 생각하면 자연스럽다. 원본 저장소는 보호하고, 기여자는 fork에서 작업한 뒤 PR을 보낸다.

작은 변경을 자주 합칠 수 있고 테스트와 배포 확인이 빠르게 돌아간다면 Trunk Based Development를 검토한다. 이 방식은 단순해 보이지만 사실 팀의 개발 습관을 많이 요구한다고 한다. 테스트, 리뷰, 배포 확인 같은 기본 절차가 받쳐줘야 한다. 이 부분은 조금 먼 이야기처럼 느껴졌지만, 오래 살아 있는 브랜치를 줄이자는 방향은 이해됐다.

참고 자료