GitFlow 전략: 브랜치를 나누니 비로소 ‘협업’이 보였다

혼자라서 괜찮다고 생각했다

지난 시간, 나는 개발 서버와 운영 서버를 분리하고, 브랜치별로 배포 경로를 다르게 설정하는 데 성공했다. develop 브랜치에 push하면 개발 서버로, master 브랜치에 merge하면 운영 서버로. 깔끔했다.

“나 혼자 개발하는데 브랜치 전략까지 필요해?”

솔직히 그렇게 생각했다. master와 develop, 두 개면 충분하다고. 기능 하나 만들고 develop에 올리고, 확인되면 master로 합치면 끝이니까. 복잡하게 브랜치를 더 나눌 이유가 뭐가 있겠나.

하지만 그 안일함은 오래가지 않았다. ‘동시에 두 가지 일이 터지는 순간’, 두 개의 브랜치로는 역부족이라는 걸 몸으로 깨닫게 되었다.

위기: 긴급 수정과 신규 기능이 동시에 터졌을 때

어느 날, 담당자로부터 새로운 기능 요청이 들어왔다. ‘대시보드에 통계 차트 추가’. 꽤 규모가 큰 작업이라 며칠은 걸릴 것 같았다. 나는 develop 브랜치에서 열심히 차트 기능을 만들기 시작했다.

그런데 이틀째 되던 날, 운영 서버에서 긴급 전화가 왔다.

“로그인 버튼이 안 눌려요! 지금 당장 고쳐주세요!”

치명적인 버그였다. 당장 고쳐서 운영 서버에 배포해야 한다. 하지만 문제가 있었다. 내 develop 브랜치에는 이미 반쯤 만든 차트 코드가 잔뜩 섞여 있었다.

선택지는 두 가지뿐이었다.

  1. develop에서 로그인 버그를 고치고 배포한다 → 미완성 차트 코드가 운영 서버에 같이 올라간다. 장애 위에 장애를 쌓는 꼴이다.
  2. 차트 코드를 전부 되돌리고(reset), 버그만 고치고 배포한 뒤, 다시 차트 코드를 복원한다 → 시간도 오래 걸리고, 복원 과정에서 코드를 날릴 위험이 크다.

사실 01편(1인 개발자 인프라 생존기)에서 이미 똑같은 실수를 한 적이 있다. 그때 master 하나에서 모든 작업을 하다가 로그인 코드와 결제 코드가 뒤엉켜 멘붕이 왔었다. develop을 분리해서 해결한 줄 알았는데, 본질적인 문제는 그대로였던 것이다.

“작업 영역이 격리되어 있지 않으면, 긴급 상황에서 대응할 수 없다.”

나는 그제서야 검색을 시작했다. ‘Git 브랜치 전략’, ‘여러 기능 동시 개발’. 수많은 글에서 하나의 이름이 반복적으로 등장했다. ‘GitFlow’.

브랜치 두 개로 버티던 평화는 ‘동시다발 요청’ 앞에서 무너졌다.

GitFlow: 작업을 ‘격리’하는 교통 체계

GitFlow는 Vincent Driessen이 2010년에 제안한 브랜치 관리 모델이다. 거창하게 들리지만 핵심은 단순하다.

‘각 작업의 성격에 맞는 전용 도로를 만들어서, 서로 부딪히지 않게 하자.’

내가 겪었던 혼란은 ‘차트 개발’과 ‘긴급 버그 수정’이라는 전혀 다른 성격의 작업이 같은 도로(develop)를 달리고 있었기 때문에 발생한 것이다. GitFlow는 이 문제를 다섯 종류의 브랜치로 해결한다.

다섯 개의 도로, 각각의 역할

브랜치 비유 역할
master 고속도로 오직 운영에 배포된 코드만 존재. 직접 수정 절대 금지.
develop 일반 도로 다음 버전을 준비하는 통합 도로. 모든 기능이 이곳에 합류(Merge).
feature/* 공사 구간 새 기능 개발용 임시 도로. develop에서 분기 → 완성 후 develop에 합류 → 삭제.
release/* 검수 구간 출시 전 최종 점검용 임시 도로. 버그 수정/버전 태깅만 허용. master + develop에 합류.
hotfix/* 긴급 우회로 운영 서버 치명적 버그 긴급 수정. master에서 분기 → master + develop에 합류.
GitFlow는 작업의 성격에 따라 전용 도로를 배정하는 교통 체계다.


아까 그 위기, GitFlow로 다시 풀어보자

GitFlow를 알기 전의 나는 develop 하나에서 모든 걸 했다. 하지만 GitFlow를 적용하면 아까의 상황이 어떻게 달라지는지 보자.

상황: 차트 기능 개발 중 + 운영 서버 긴급 버그 발생

1단계: feature 브랜치로 기능 격리

차트 기능 시작 시, develop이 아닌 ‘feature/chart’ 브랜치를 만들어서 작업한다.

# develop에서 feature 브랜치를 분리한다
git checkout develop
git checkout -b feature/chart

이제 차트 코드는 feature/chart에만 존재하고, develop은 깨끗한 상태를 유지한다.

2단계: hotfix 브랜치로 긴급 대응

긴급 버그 발생! 당황하지 않고 master에서 ‘hotfix/login-fix’ 브랜치를 만든다.

# master에서 hotfix 브랜치를 분리한다
git checkout master
git checkout -b hotfix/login-fix

차트 코드와 완전히 격리된 공간에서 로그인 버그만 수정한다.

3단계: hotfix를 양쪽에 합치기

수정이 끝나면 hotfix를 master와 develop 양쪽에 합친다.

# master에 합치고 배포한다
git checkout master
git merge hotfix/login-fix

# develop에도 합쳐서 다음 버전에도 수정 사항을 반영한다
git checkout develop
git merge hotfix/login-fix

# hotfix 브랜치는 삭제한다
git branch -d hotfix/login-fix

운영 서버는 즉시 복구되고, 차트 기능은 feature/chart에서 아무 영향 없이 계속 개발할 수 있다. 이것이 ‘격리’의 힘이다.


격리가 없으면 혼란, 격리가 있으면 질서. GitFlow의 핵심은 ‘작업의 독립성 보장’이다.

GitLab CI에 GitFlow 연동하기

지난 글에서 우리는 develop은 개발 서버에 자동 배포, master는 운영 서버에 수동 배포하도록 파이프라인을 설정해뒀다. GitFlow를 도입해도 이 설정은 거의 그대로 유지된다. 다만, feature 브랜치에서도 CI(테스트)는 돌아가게 하고 싶다.

기존 .gitlab-ci.yml에 feature 브랜치 대응을 추가하면 이렇게 된다.

stages:
  - test
  - build
  - deploy

# [1단계] 테스트: 모든 브랜치에서 실행
# feature, develop, master 어디서든 push하면 테스트가 돌아간다.
test_job:
  stage: test
  image: openjdk:17-alpine
  script:
    - ./gradlew test

# [2단계] 빌드: develop과 master에서만 실행
build_job:
  stage: build
  script:
    - docker build -t my-reg/app:$CI_COMMIT_SHA .
    - docker push my-reg/app:$CI_COMMIT_SHA
  only:
    - develop
    - master

# [3단계] 개발 서버 배포 (자동)
deploy_to_dev:
  stage: deploy
  script:
    - ssh user@dev-server "docker service update --image my-reg/app:$CI_COMMIT_SHA app-dev"
  only:
    - develop

# [4단계] 운영 서버 배포 (수동 승인)
deploy_to_prod:
  stage: deploy
  script:
    - ssh user@prod-server "docker service update --image my-reg/app:$CI_COMMIT_SHA app-prod"
  only:
    - master
  when: manual

핵심 변경점은 ‘test_job’이다. only 조건을 달지 않았기 때문에 어떤 브랜치에서 push하든 테스트가 돌아간다. 즉, feature/chart 브랜치에서 코드를 올릴 때마다 “이 코드가 기존 로직을 망가뜨리지 않는지” 기계가 먼저 확인해준다.

빌드와 배포는 여전히 develop과 master에서만 실행된다. feature 브랜치의 코드가 develop에 merge 되기 전까지는 서버에 올라갈 일이 없으니, 안전망은 유지하면서도 개발 속도는 떨어지지 않는다.

정리하면 이렇다.

브랜치 테스트(CI) 빌드 배포
feature/* ✅ 자동
develop ✅ 자동 ✅ 자동 ✅ 개발 서버 자동
master ✅ 자동 ✅ 자동 ✅ 운영 서버 수동 승인

실무 조언: GitFlow, 만능은 아니다

GitFlow를 실제로 써보면서 느낀 점들을 솔직하게 정리한다.

1인 개발에서도 의미가 있을까?

결론적으로, 있다. 혼자 개발하더라도 ‘긴급 수정’과 ‘신규 기능 개발’이 동시에 발생하는 상황은 반드시 온다. 그때 feature와 hotfix 브랜치의 존재가 나를 구해준다. 최소한 feature 브랜치만이라도 습관화하는 것을 추천한다.

release 브랜치는 언제 쓸까?

솔직히 소규모 프로젝트나 1인 개발에서는 release 브랜치를 쓸 일이 거의 없었다. release는 QA 팀이 별도로 검수하거나 버전 번호(v1.2.0)를 태깅하는 절차가 필요한 조직에서 빛을 발한다. 혼자 개발한다면 develop에서 master로 바로 merge 해도 무방하다.

브랜치 이름은 규칙적으로

나중에 어떤 브랜치가 무슨 작업인지 헷갈리지 않으려면 이름 규칙을 정해두자.

타입 네이밍 규칙 예시
feature feature/기능명 feature/chart-dashboard, feature/user-profile
hotfix hotfix/이슈번호-설명 hotfix/142-login-button-fix
release release/버전번호 release/1.3.0

GitFlow 말고 다른 전략은?

GitFlow가 유일한 정답은 아니다. 프로젝트 규모와 배포 주기에 따라 더 적합한 전략이 있을 수 있다.

전략 특징 적합한 환경
GitFlow master + develop + feature/release/hotfix 명확한 릴리즈 주기가 있는 SI/솔루션
GitHub Flow master + feature만 사용. 단순함. 배포가 빈번한 웹 서비스(SaaS)
Trunk-Based 거의 모든 작업을 main에서 직접 진행 CI/CD가 극도로 잘 갖춰진 대규모 팀

나처럼 ‘명확한 출시(릴리즈) 주기’가 있는 SI/솔루션 프로젝트라면 GitFlow가 잘 맞는다. 반면 매일 수십 번 배포하는 웹 서비스라면 GitHub Flow가 더 가벼울 수 있다. 정답은 없고, 우리 팀의 상황에 맞는 전략을 고르면 된다.


마치며: 브랜치를 나누는 것은 ‘미래의 나’를 지키는 일이다

처음엔 "브랜치를 왜 이렇게 많이 나눠?"라고 투덜거렸다. 혼자 개발하는데 master, develop, feature, hotfix까지 관리하는 게 과하다고 느꼈다.

하지만 긴급 상황이 터졌을 때, 차분하게 hotfix 브랜치를 만들고 개발 중인 기능에 영향 없이 버그를 수정하는 순간, 깨달았다. 이 브랜치 전략은 ‘지금의 편의’를 위한 것이 아니라 ‘미래의 위기’에 대비하는 안전장치라는 것을.

돌이켜보면, 이번 시리즈 전체가 그랬다.

  • 01편에서 수동 배포의 한계를 느꼈고,
  • 02편에서 파이프라인이라는 설계도를 그렸고,
  • 03편에서 러너라는 일꾼을 채용했고,
  • 04편에서 환경을 분리해서 안전망을 깔았고,
  • 05편에서 브랜치를 나눠 작업을 격리했다.

이 모든 과정은 결국 하나의 목표를 향하고 있었다. ‘사람의 실수를 시스템으로 막자.’

다음에는 기능 구현을 넘어, 만든 서비스가 ‘잘’ 돌아가도록 만드는 이야기를 시작한다. 캐싱, 리버스 프록시, 모니터링. 서비스를 지키는 또 다른 안전장치들의 세계로 들어가 보자.

댓글 남기기