배포 준비와 다음 기능 개발을 병렬로 진행하기 위한 Git 브랜치 전략
4명의 백엔드 개발자가 함께 만든 1차 MVP가 배포를 앞두고 있었다. 기능 구현은 대부분 끝났지만 Docker와 Nginx, GitHub Actions, 환경 변수, AWS 설정은 실제 환경에서 맞춰 봐야 했다. 동시에 팀은 2차 확장 기능 개발도 시작해야 했다.
처음 든 생각은 단순했다. 배포할 코드가 develop에 있으니 먼저 main에 병합하고 배포 준비를 하면 되지 않을까? 하지만 배포 준비도 코드를 바꾸는 작업이다. 운영 프로파일이나 프록시 경로가 틀릴 수 있고, 헬스 체크 엔드포인트와 워크플로도 손봐야 할 수 있다. 검증 전 코드를 main에 올리면 main이 운영 코드의 기준이라는 약속부터 흐려진다.
그렇다고 배포가 끝날 때까지 나머지 팀원들이 기다릴 수도 없었다. 이번에는 Git Flow 전체를 도입하는 대신, 지금 필요한 릴리스 브랜치 개념만 빌려 deploy/m2를 두기로 했다.
배포를 앞두고 생긴 고민
가장 먼저 정해야 했던 것은 main에 병합하는 시점이었다. develop을 미리 병합하면 배포할 커밋이 명확해 보인다. 그러나 Dockerfile, Nginx 프록시, GitHub Actions 워크플로처럼 배포 중에 확인할 항목은 아직 검증되지 않았다. 이 상태는 배포가 준비된 것이 아니라 배포 준비를 시작한 상태에 가깝다.
더 큰 문제는 develop의 다음 행선지였다. 배포 담당자가 설정을 고치는 동안 다른 팀원들은 2차 기능을 계속 병합한다. 그러면 오전에 검증한 코드와 오후에 다시 배포할 코드가 달라진다. 미완성 기능이 들어오거나 배포 직전에 의존성이 바뀌면, 발생한 오류가 배포 설정 때문인지 신규 기능 때문인지 구분하기도 어려워진다.
하나의 브랜치에 두 목적을 담는 순간 팀의 작업 기준도 엇갈린다. 배포 담당자는 변경을 최소화하고 싶고, 기능 담당자는 새 코드를 자주 통합하고 싶다. 둘 다 필요한 행동이지만 같은 브랜치에서는 충돌 가능성과 커뮤니케이션 비용을 키운다. 배포 안정화와 기능 통합은 변경을 대하는 태도부터 달랐다.
하나의 develop 브랜치로는 어려웠던 이유
develop 하나만 사용하면 배포 대상이 계속 움직인다. 검증 중인 버전에 아직 끝나지 않은 2차 기능이 섞일 수 있고, 테스트 기준도 커밋이 추가될 때마다 바뀐다. 배포 직전 추가된 기능 코드가 예상하지 못한 장애를 만들면 원인을 좁히는 데에도 시간이 든다.
브랜치를 나누면 관리 대상은 하나 늘어난다. 병합 순서를 맞춰야 하고, 수정사항을 되돌려 반영하는 작업도 생긴다. 반면 무엇을 배포하는지 고정할 수 있고, 새 기능 때문에 배포 검증을 처음부터 반복하는 일을 줄일 수 있다. 4명 규모의 팀에서는 이 추가 비용보다 기준점이 흔들릴 때 생기는 비용이 더 크다고 판단했다.
develop하나에 배포 설정과 신규 기능을 함께 모으면 배포 대상도 계속 바뀐다. 브랜치를 나누면 배포 코드를 고정한 채 다음 기능을 이어 갈 수 있다.
세 브랜치의 역할을 나누다
세 브랜치는 코드의 품질 등급보다 코드가 향하는 목적지로 구분했다.
main은 실제 운영 환경에 배포되었거나 즉시 배포할 수 있는 코드의 기준점이다. 장애가 발생했을 때 돌아갈 마지막 정상 버전을 찾는 곳이기도 하다. 그래서 배포 가능 여부를 확인하기 전에는 변경하지 않는다.
develop은 다음 버전 기능이 합쳐지는 개발 브랜치다. deploy/m2가 분기된 뒤에도 2차 확장 기능은 이곳에서 계속 통합한다.
deploy/m2는 특정 시점의 MVP 코드를 고정하고 배포를 준비하는 브랜치다. Docker, Nginx, GitHub Actions, 환경 변수 연결처럼 배포에 필요한 설정과 배포 과정에서 발견한 오류만 다룬다. 장기간 유지하는 상시 브랜치가 아니라 해당 배포가 끝나면 삭제하는 임시 릴리스 브랜치에 가깝다.
1
2
3
4
5
6
7
8
9
10
11
12
13
main
└─ 현재 운영 중인 버전
develop
└─ 다음 확장 기능 개발
deploy/m2
├─ 현재 배포할 MVP 코드
├─ Docker 설정
├─ Nginx 설정
├─ GitHub Actions 배포 워크플로
├─ 환경 변수 연결
└─ 배포 과정에서 발견된 오류 수정
배포와 기능 개발을 병렬로 진행하는 흐름
배포 기준으로 삼을 커밋이 정해지면 최신 develop에서 deploy/m2를 만든다.
1
2
3
4
5
git switch develop
git pull origin develop
git switch -c deploy/m2
git push -u origin deploy/m2
이 시점부터 흐름은 둘로 나뉜다. 배포 담당자는 deploy/m2에서 설정과 오류를 고치고, 나머지 팀원은 develop을 기준으로 기능 브랜치를 만들어 2차 개발을 이어 간다. 배포 브랜치에는 새 기능이나 대규모 리팩터링을 넣지 않는다. 패키지 구조 정리, 갑작스러운 데이터베이스 구조 변경, 다음 버전 기능의 선행 병합도 같은 이유로 피한다. 배포 안정화에 꼭 필요한 최소 변경만 허용해야 검증 범위를 통제할 수 있다.
deploy/m2는 배포 대상을 고정하고,develop은 다음 기능을 계속 받는다. 배포가 끝나면 배포 수정사항을 두 기준 브랜치에 반영한다.
검증이 끝난 deploy/m2는 main에 병합한다. 아래 명령은 흐름을 보여 주기 위한 예시다.
1
2
3
4
git switch main
git pull origin main
git merge --no-ff deploy/m2
git push origin main
실제 팀 저장소에서는 로컬에서 바로 병합하거나 push하지 않는다. deploy/m2에서 main으로 Pull Request를 열고 코드 리뷰와 CI를 통과한 뒤 병합하는 편이 안전하다. 이전에 PR을 배포 실패를 막는 검증 지점으로 정리한 글에서도 다뤘듯, 배포 단계가 처음 오류를 발견하는 장소가 되어서는 안 된다.
main에 미리 병합하지 않기로 한 이유
main은 배포 준비가 시작됐음을 알리는 브랜치가 아니다. 우리 팀에서는 실제 배포가 끝났거나 즉시 배포 가능한 상태가 확인된 코드를 가리킨다. 운영 환경은 로컬이나 테스트 환경과 다르므로 설정 오류, 환경 변수 누락, 네트워크 경로 차이가 검증 중에 드러날 수 있다.
미리 병합하면 검증되지 않은 커밋이 main에 머문다. 그사이 운영 장애가 생기면 현재 main이 정상 배포 버전인지, 아직 준비 중인 버전인지부터 확인해야 한다. 복구 기준점이 모호해지는 셈이다. CI/CD와 배포·복구 흐름을 정리했던 글에서 확인했듯, 빌드 성공이나 컨테이너 실행만으로 배포 성공을 판단할 수도 없다. 운영 헬스 체크까지 통과해야 비로소 기준점을 옮길 수 있다.
물론 모든 팀이 main을 같은 뜻으로 사용하지는 않는다. 배포 예정 코드를 main에 먼저 모으고 태그나 별도 브랜치로 운영 버전을 관리하는 방식도 가능하다. 다만 현재 4인 팀에서는 main을 운영 코드로 단순하게 유지하는 편이 설명하기 쉽고, 장애 대응 때 판단할 것도 적다. 우리 팀의 규모와 배포 절차에 맞춘 선택이지 유일한 정답은 아니다.
배포 수정사항을 develop에 다시 반영해야 하는 이유
배포 과정에서는 생각보다 많은 코드 변경이 생긴다. 운영 프로파일을 고치거나 환경 변수 이름을 바꾸고, Dockerfile·Docker Compose·Nginx 프록시 경로를 수정할 수 있다. 헬스 체크 엔드포인트, GitHub Actions 워크플로, CORS와 보안 설정도 실제 환경에서 문제를 발견하기 쉽다.
이 변경이 main에만 남으면 main과 develop의 차이가 쌓인다. 다음 릴리스가 develop에서 출발할 때 이미 고쳤던 설정 오류가 다시 나타날 수 있다. 이런 브랜치 간 드리프트는 눈에 잘 띄지 않다가 다음 배포에서 같은 장애와 재작업으로 돌아온다.
그래서 deploy/m2의 수정사항을 develop에도 병합한다.
1
2
3
4
git switch develop
git pull origin develop
git merge --no-ff deploy/m2
git push origin develop
이 작업 역시 실제로는 deploy/m2에서 develop을 대상으로 Pull Request를 만들어 처리한다. 배포 브랜치의 변경 대부분이 다음 버전에도 필요하다면 merge가 기본 선택이다. 일부 커밋만 필요할 때는 cherry-pick을 쓸 수 있지만, 커밋이 중복되고 어떤 수정이 옮겨졌는지 추적하기 복잡해진다. 특히 경험이 많지 않은 팀에서는 필요한 커밋 하나를 빠뜨릴 위험도 있다.
현재 프로젝트의 배포 수정은 대부분 다음 버전에서도 유지해야 한다. 따라서 deploy/m2 전체를 develop에 병합하고, 정말 제외해야 할 변경이 있을 때만 별도로 판단하기로 했다. 예전에 로컬 develop이 오염되어 PR에 다른 커밋이 섞였던 경험도 있었기 때문에, 역반영 전에는 원격 기준 브랜치를 갱신하고 PR의 커밋과 파일 목록을 확인한다.
현재 팀에 맞는 운영 규칙
브랜치를 세 개로 나눴다고 흐름이 저절로 안전해지는 것은 아니다. 누가 어디에 무엇을 병합할 수 있는지 팀 규칙으로 고정해야 한다. 우리 팀은 다음 정도를 현실적인 출발점으로 잡았다.
main과develop에는 직접 push하지 않는다.- 모든 변경은 Pull Request로 병합하고 CI 성공을 필수 조건으로 둔다.
- 최소 1명, 중요한 배포 변경은 가능하면 2명의 승인을 받는다.
deploy/m2에는 신규 기능을 병합하지 않는다.- 배포가 끝나면
deploy/m2의 수정사항을develop에 역반영한다. - 운영 버전에는 Git 태그를 남기고 역반영이 끝난 배포 브랜치는 삭제한다.
- 긴급 운영 수정은
hotfix/*에서 처리한 뒤main과develop양쪽에 반영한다.
브랜치 보호와 리뷰 승인 규칙은 GitHub Ruleset과 코드 리뷰를 다룬 기록처럼 도구로 강제할 수 있다. 다만 승인 인원을 늘리면 안전성은 높아지는 대신 작은 팀의 병합 대기 시간도 길어진다. 평소에는 1명, 운영과 보안에 영향을 주는 변경은 2명처럼 위험도에 따라 기준을 나누는 편이 지금 팀에는 맞아 보였다.
배포가 끝난 커밋에는 복구와 추적을 위한 태그를 남긴다.
1
2
git tag -a v1.0.0 -m "MVP production release"
git push origin v1.0.0
이번에 배운 점
처음에는 브랜치를 더 만들면 관리만 복잡해진다고 생각했다. 실제로 병합과 역반영, PR 검토가 늘어나는 것은 맞다. 하지만 배포 준비와 기능 개발은 목적이 다르다. 전자는 대상을 고정하고 변화를 줄여야 하며, 후자는 여러 변경을 계속 통합해야 한다. 서로 반대되는 작업을 한 브랜치에 억지로 담는 편이 오히려 더 비쌌다.
이번 배포에서는 main을 운영 코드의 기준점으로 유지하고, 배포 대상은 deploy/m2에서 고정해 검증하기로 했다. 2차 기능은 develop에서 계속 개발하며, 배포 중 발견한 수정은 반드시 다시 develop에 반영한다.
브랜치 전략의 목적은 브랜치 수를 늘리는 데 있지 않다. 배포할 코드와 앞으로 개발할 코드를 팀원 모두가 같은 기준으로 구분하게 만드는 데 있다. 현재 규모에서는 Git Flow 전체를 엄격히 도입하기보다 필요한 릴리스 브랜치만 선택해서 쓰는 방식이 운영 부담과 안전성 사이의 균형에 더 가까웠다.

