LabHub
배우기 러닝패스 코스

CI/CD 파이프라인 · 트렁크 기반 개발과 브랜치 보호 · 이론

초록불이 병합 뒤에도 초록불인가

LabHub 에서 이어서 보기

한 줄 요약

브랜치에서 초록불이었는데 병합하고 나니 빨간불이 되는 일은 드물지 않다. 브랜치의 검사는 내 변경 더하기 그때의 트렁크를 본 것이지, 병합된 뒤의 트렁크를 본 것이 아니기 때문이다. 이 간극을 줄이는 것이 짧은 수명 브랜치와 서버 쪽 규칙이 하는 일이다.

왜 이게 필요했나

트렁크 기반 개발은 "개발자들이 트렁크라 불리는 하나의 브랜치에서 협업하고, 다른 장수 개발 브랜치를 만들려는 압력에 저항하는 소스 관리 모델" 로 정의된다. 짧은 수명 브랜치는 코드 리뷰와 빌드 검사를 위해 쓰되 곧 사라지는 것을 전제로 한다. 릴리스 브랜치도 필요할 때 트렁크에서 잘라 쓰고 릴리스 뒤 얼마 지나 지운다. 릴리스에 붙이는 버전 이름에도 같은 성질이 요구된다. 시맨틱 버전 규약은 한 번 배포된 버전의 내용은 수정되어서는 안 되며 고칠 것이 있으면 새 버전으로 내야 한다고 못 박는다. 이미 나간 것을 손대지 않는 쪽이 되돌릴 대상을 남긴다.

왜 장수 브랜치를 피하는가. 브랜치가 길어질수록 트렁크와의 거리가 벌어지고, 벌어진 거리는 병합할 때 한꺼번에 청구된다. 그리고 그 청구서에서 무서운 것은 텍스트 충돌이 아니다. 텍스트 충돌은 도구가 알려 준다. 무서운 것은 의미 충돌 이다. 내가 브랜치에서 쓰고 있던 함수의 동작을 다른 사람이 트렁크에서 바꿨다면, 두 변경은 서로 다른 줄에 있어서 깨끗하게 병합되고 결과만 틀린다. 버전 관리 도구는 이것을 감지하지 못한다.

되먹임 관점에서도 답은 같다. 통합이 미뤄질수록 문제를 늦게 알고, 늦게 알수록 원인 후보가 많아진다. "어떤 코드도 두어 시간 넘게 통합되지 않은 채로 있지 않는다" 는 켄트 벡의 문장이 목표선을 가장 짧게 적어 준다.

브랜치 보호는 서버 쪽 규칙이어야 한다

규칙을 사람의 약속으로 두면 지켜지지 않는다. 바쁜 날, 장애 대응 중, 새로 온 사람의 첫 주에 깨진다. 그래서 규칙은 밀어 넣는 쪽이 아니라 받는 쪽 에 둔다.

git 자체가 이 구분을 갖고 있다. 훅은 기본적으로 $GIT_DIR/hooks 에 있고, 복제로 따라오지 않는다. git init 이 템플릿을 복사할 수는 있지만, 내가 만든 pre-commit 훅이 동료의 사본에 저절로 생기지는 않는다. 즉 로컬 훅은 편의 장치이지 강제 수단이 아니다. 강제는 서버 쪽 훅이 한다.

호스팅 서비스의 브랜치 보호도 같은 자리의 기능이다. GitHub 의 보호 규칙에는 병합 전 풀 리퀘스트 요구, 필수 상태 검사, 대화 해결 요구, 서명된 커밋 요구, 선형 이력 요구, 병합 대기열 요구, 배포 성공 요구, 브랜치 잠금, 밀어 넣을 수 있는 사람 제한, 강제 푸시 허용, 삭제 허용이 있다.

여기서 반드시 알아야 할 기본값이 하나 있다. 보호 규칙은 기본적으로 관리자에게 적용되지 않는다. 문서는 저장소 관리자 권한이나 보호 우회 권한을 가진 사람에게는 제약이 적용되지 않는다고 적고, "위 설정의 우회를 허용하지 않음" 을 켜야 관리자에게도 적용된다고 안내한다. 규칙을 켜 놓고 안심하고 있었는데 정작 가장 위험한 변경이 그 규칙을 지나쳐 가는 일이 여기서 생긴다.

초록불이 병합 뒤에 빨간불이 되는 문제

필수 상태 검사를 켜도 간극은 남는다. 브랜치 A 와 브랜치 B 가 각각 트렁크 기준으로 초록이었는데, A 를 먼저 병합하고 나면 B 의 검사 결과는 더 이상 현재 트렁크에 대한 것이 아니다. 그대로 병합하면 트렁크가 깨진다.

해결의 방향은 두 가지다.

긴 브랜치 대신 기능 플래그

"이 기능은 아직 완성이 아니라서 병합할 수 없다" 가 장수 브랜치의 흔한 이유다. 답은 완성되지 않은 것을 트렁크에 넣되 켜지 않는 것 이다. 기능 플래그와 추상화에 의한 분기(branch by abstraction)가 그 방법이다. 미완성 코드가 트렁크에 있되 사용자에게는 보이지 않으므로, 통합은 매일 하면서 공개 시점은 따로 정할 수 있다.

대신 플래그에는 비용이 있다. 플래그가 늘면 실제로 도는 조합이 늘고, 시험이 커버하지 못하는 경로가 생긴다. 플래그를 만들 때 지우는 시점을 같이 정해 두는 것 이 유일하게 통하는 관리법이다.

현장에서 만나는 모습

참고

다음 실습에서 할 것

로컬에 베어 저장소를 만들어 받는 쪽 규칙을 직접 세운다. pre-receiveupdate 훅을 써서 보호 브랜치로의 직접 푸시를 막고, 특정 레퍼런스만 거부하는 것과 푸시 전체를 거부하는 것의 차이를 종료 코드로 확인한다. 그다음 로컬 훅을 만들어 두고 복제한 사본에 그 훅이 따라오지 않는 것을 눈으로 본다. 필수 검사 흉내는 커밋에 붙인 검사 결과를 훅이 확인하는 방식으로 만들고, 의미 충돌은 두 브랜치가 각각 통과한 뒤 병합했을 때 깨지는 예제로 재현한다. 마지막에는 병합 전에 트렁크를 반영해 다시 검사하는 대기열 흉내를 스크립트로 만들어, 줄이 길어질 때 검사 횟수가 어떻게 달라지는지 센다.