There Is Only One Question That Picks a Strategy
한국어 원문으로 표시합니다.
한 줄 요약
브랜치 전략을 고르는 질문은 "어떤 그림이 예쁜가"가 아니라 "장애가 났을 때 이 저장소에서 원인 커밋을 어떻게 찾을 계획인가"다.
왜 이게 필요했나
Git Flow, GitHub Flow, Trunk-Based 는 서로 다른 배포 주기를 전제로 만들어졌다. 전제를 무시하고 이름만 가져오면 매일 마찰이 생긴다.
| 전략 | 브랜치 종류 | 병합 빈도 | 맞는 곳 |
|---|---|---|---|
| Git Flow | 5종 이상 | 주 1~2회 | 정기 릴리스, 모바일 앱, 패키지 |
| GitHub Flow | 2종 | 일 1~3회 | CD 를 실천하는 SaaS |
| Trunk-Based | 1~2종 | 일 5~10회 이상 | 브랜치 수명 1일 이내, Feature Flag 필수 |
Trunk-Based 는 CI 요구 수준이 매우 높다. Feature Flag 없이 시작하면 미완성 코드가 그대로 배포된다. 반대로 GitOps 환경에서 Git Flow 의 release 브랜치는 자동 동기화 패턴과 마찰이 크다. 그래서 GitOps 를 쓴다면 GitHub Flow 또는 Trunk-Based 를 권한다.
공통 규칙 하나는 어디서나 통한다. 3일 이상 살아 있는 feature 브랜치는 merge conflict 의 온상이다.
어떻게 동작하나
병합 방식은 세 가지고 각각 다른 것을 남긴다.
--no-ff머지: 부모가 둘인 커밋이 생겨 "이 묶음이 언제 통합됐는가"가 이력에 남는다.- 빨리 감기(fast-forward): 머지 커밋 없이 브랜치 포인터만 앞으로 간다. 이력은 일직선이 되지만 통합 시점은 남지 않는다.
- 스쿼시 머지: 커밋을 하나로 눌러 붙인다. 깔끔해 보이지만 세 가지를 가져간다. 이분 탐색 해상도가 PR 크기로 고정되고, 체리픽 정밀도를 잃고, Git 입장에서 그 브랜치는 머지된 적이 없다(새 커밋의 부모가 원본 브랜치가 아니기 때문). 그래서 스쿼시 머지를 쓰는 팀은 머지 직후 소스 브랜치를 지운다는 규칙을 함께 가져가야 한다.
체리픽은 저장된 diff 를 오려 붙이는 것이 아니다. Git 은 애초에 diff 를 저장하지 않고 스냅숏만 저장한다. 그래서 대상 커밋의 부모 트리를 공통 조상으로 삼아 삼자 병합을 수행한다. 체리픽이 충돌하는 이유가 여기에 있다.
현장에서 만나는 모습
핫픽스를 릴리스 브랜치에서 고치고 main 에 반영하지 않아 다음 릴리스에서 같은 버그가 되살아나는 일은 어느 팀에나 한 번은 생긴다. 릴리스 브랜치를 운영한다면 "핫픽스는 반드시 main 으로도 가져온다"가 정책 문장으로 적혀 있어야 한다.
태그도 마찬가지다. 가벼운 태그는 그냥 포인터라 누가 언제 왜 붙였는지 남지 않는다. 릴리스 태그는 -a 로 주석을 붙여야 나중에 조사에 쓸 수 있다.
이력은 조사를 위해 남긴다
브랜치 전략의 값어치는 평상시가 아니라 사고가 났을 때 드러난다. "어제까지 되던 것이 오늘 안 된다" 는 상황에서 원인 커밋을 찾는 표준 도구는 이분 탐색이다. Git 은 git bisect 로 이것을 반자동으로 해 준다. 정상인 커밋과 고장 난 커밋을 알려 주면 중간 지점을 체크아웃해 주고, 사람이 좋다/나쁘다만 답하면 범위를 반씩 줄인다. 커밋 1,000개 중 원인을 찾는 데 열 번이면 충분하다.
이분 탐색이 잘 되려면 이력이 두 가지 조건을 만족해야 한다. 커밋 하나하나가 빌드되고 동작해야 하고, 커밋의 크기가 작아야 한다. 중간에 동작하지 않는 커밋이 섞여 있으면 사람이 좋다/나쁘다를 답할 수 없어 그 지점을 건너뛰어야 하고, 스쿼시 머지로 PR 하나가 커밋 하나가 되어 있으면 탐색이 그 PR 앞에서 멈춘다. "이 PR 안 어딘가" 까지만 알아내고 나머지는 손으로 읽어야 한다는 뜻이다. 앞에서 스쿼시 머지가 이분 탐색 해상도를 PR 크기로 고정한다고 한 말이 이 대목이다.
원인 커밋을 찾은 다음의 선택도 미리 정해 두어야 한다. 되돌리는 방법은 둘이고 성질이 완전히 다르다.
git revert는 그 변경을 취소하는 새 커밋을 만든다. 이력이 보존되고, 이미 공유된 브랜치에서도 안전하다. 운영 브랜치에서는 이쪽만 쓴다.git reset은 브랜치 포인터를 뒤로 옮긴다. 공유된 브랜치에서 하면 다른 사람의 저장소와 이력이 갈라져, 그 뒤로 모두가 강제 푸시와 충돌에 시달린다.
머지 커밋을 되돌릴 때는 한 가지가 더 필요하다. 부모가 둘이므로 어느 쪽 줄기를 유지할지 -m 1 처럼 지정해야 하고, 보통 1번이 병합을 받은 쪽(main)이다. 그리고 머지를 되돌린 뒤에 같은 브랜치를 다시 병합하면 아무 변경도 들어오지 않는다. Git 이 보기에는 이미 병합된 커밋들이기 때문이다. 되돌린 것을 다시 넣으려면 되돌림 커밋을 한 번 더 되돌려야 한다는 사실을, 급한 상황에서 처음 배우지 않는 편이 좋다.
다음 실습에서 할 것
저장소를 처음부터 만들고, 기능 브랜치를 --no-ff 로 병합해 통합 기록을 남기고, 작은 수정은 빨리 감기로 올리고, 릴리스 태그를 붙이고, 릴리스 브랜치의 핫픽스를 체리픽으로 main 에 반영한 뒤, 병합된 브랜치만 골라 정리합니다. 마지막에는 이 저장소의 실제 숫자를 세어 전략 비교표를 씁니다.