트렁크 기반 개발이란? 새벽 3시 머지 지옥을 끝내는 법

요약

트렁크 기반 개발은 모든 개발자가 작은 변경을 공유 브랜치에 하루에 최소 한 번 머지하고, 그 브랜치를 항상 배포 가능하게 유지하는 방식입니다. 활성 브랜치는 3개 이하, 브랜치 수명은 몇 시간, 빌드와 테스트는 몇 분 안에 끝나야 합니다. 미완성 기능은 기능 플래그로 숨기고, 릴리스는 트렁크에서 바로 하거나 짧은 릴리스 브랜치로 자릅니다. 전제 조건은 빠른 CI, 신뢰할 수 있는 테스트, 그리고 몇 분 안에 감지하고 되돌릴 수 있는 관측성입니다.

나무 줄기에 짧은 가지들이 다시 합쳐지는 모습, 트렁크 기반 개발을 표현한 이미지

새벽 3시입니다. 릴리스 브랜치는 머지되지 않습니다. 3주 전에 만든 커밋 40개가 main과 같은 파일을 건드리고 있고, 누구도 충돌을 어디서부터 풀어야 할지 모릅니다. 이 고통이 트렁크 기반 개발이 필요한 이유입니다. 트렁크 기반 개발이란 정확히 무엇일까요? 모든 개발자가 작은 변경을 하나의 공유 브랜치(trunk 또는 main)에 하루에 최소 한 번 머지하고, 그 브랜치를 언제든 배포 가능한 상태로 유지하는 브랜치 모델입니다.

이 글은 Git에 이미 익숙한 플랫폼 엔지니어를 위한 글입니다. 이 모델이 요구하는 것, 이 모델이 막아 주는 문제, 그리고 이 모델이 조용히 깨지는 지점을 다룹니다.

운영 관점에서 트렁크 기반 개발이란 무엇인가?

정의를 제약 조건으로 풀어 보겠습니다. 모든 개발자는 단일 브랜치에 통합합니다. 그 밖의 브랜치는 몇 주가 아니라 몇 시간만 살아 있습니다. 트렁크는 모든 커밋에서 빌드와 테스트를 통과하므로 언제든 배포할 수 있습니다.

DORA의 역량 페이지는 이를 수치로 제시합니다. 저장소의 활성 브랜치는 3개 이하, 트렁크로의 머지는 하루에 최소 한 번, 코드 동결은 없음, 빌드와 테스트 주기는 몇 분 이내입니다. 이 숫자들이 핵심입니다. 이 모델은 브랜칭 스타일의 취향이 아니라 피드백 루프의 예산입니다.

어두운 밤 책상 위의 노트북 터미널과 켜진 휴대폰 알림

소규모 팀은 트렁크에 직접 커밋하기도 합니다. 규모가 큰 팀은 리뷰와 빌드 검증을 위해 수명이 짧은 브랜치와 풀 리퀘스트를 쓰지만, 통합을 미루기 위해 쓰지는 않습니다. trunkbaseddevelopment.com 레퍼런스는 두 방식을 모두 설명하며, Google이 모노레포의 단일 트렁크에서 약 35,000명의 개발자를 운영한다는 사례를 인용합니다.

장기 브랜치는 예측 가능한 일정에 따라 실패합니다

기능 브랜치는 빌린 돈입니다. 이자는 머지 충돌이고, 브랜치를 떠나 있는 동안 main에 커밋이 쌓일 때마다 이자가 복리로 붙습니다. 브랜치가 오래 살수록 diff는 커지고, diff가 커질수록 누구도 꼼꼼히 읽지 않게 됩니다.

이것이 변경 실패율에 어떤 영향을 주는지 봅시다. 2,000줄짜리 머지는 대충 훑고 승인됩니다. 60줄짜리 머지는 실제로 읽힙니다. 리뷰어가 게을러서가 아닙니다. 주의력을 나눠 쓰고 있는 것입니다. 리뷰 품질을 확장할 수 있는 유일한 방법은 작은 배치입니다.

두 번째 비용은 릴리스 전까지 보이지 않습니다. 각각 CI를 통과한 두 브랜치도 만나는 순간 서로를 깨뜨릴 수 있습니다. 통합 시점, 즉 마감이 걸린 가장 나쁜 순간에 그 사실을 알게 됩니다.

트렁크를 신뢰하려면 무엇이 먼저 필요한가

지원 관행 없이 트렁크로 옮기면, 팀은 한 분기 안에 기능 브랜치로 되돌아갑니다. 먼저 네 가지가 갖춰져야 합니다.

이 중 하나라도 빠지면 트렁크는 문제를 잡아내는 곳이 아니라 문제가 쌓이는 곳이 됩니다.

완성되지 않은 작업을 배포하지 않고 머지하는 방법은?

회의적인 사람들이 반드시 던지는 질문이고, 답은 평범합니다. 배포와 릴리스를 분리하는 것입니다. 코드는 꺼진 상태로 프로덕션에 도착합니다. 누구에게 보일지는 플래그가 결정합니다.

// checkout.ts
import { flags } from "./flags";

export async function renderCheckout(user: User) {
  if (await flags.isEnabled("new-payment-flow", { userId: user.id })) {
    return renderNewPaymentFlow(user); // 트렁크에 머지됨, 기본값은 꺼짐
  }
  return renderLegacyCheckout(user);
}

새 흐름은 첫날 꺼진 플래그 뒤에서 머지됩니다. 먼저 내부 사용자에게, 다음에 1퍼센트, 그다음 10퍼센트에 켜고, 각 단계마다 SLO 게이트를 둡니다. 에러율이 예산을 넘으면 플래그를 끕니다. 배포 없이 사실상 되돌린 것입니다. 호스팅형 플래그 서비스를 검토하는 팀은 보통 LaunchDarkly부터 살펴보지만, 이 패턴은 어떤 제공자나 사내 설정 저장소로도 동작합니다.

두 번째 기법은 대규모 리팩터링을 위한 추상화 뒤의 분기(branch by abstraction)입니다. 인터페이스를 도입하고, 호출부를 그 인터페이스로 돌리고, 새 구현을 그 뒤에 만든 다음 준비되면 교체합니다. 장기 브랜치도, 빅뱅 머지도 없습니다.

벽면 스위치 줄, 일부는 켜져 있고 일부는 꺼져 있는 모습, 기능 플래그를 상징

솔직한 비용: 플래그는 반감기가 있는 부채입니다

컨퍼런스에서는 이 얘기를 잘 하지 않습니다. 플래그를 하나 추가할 때마다 프로덕션에 조건문이 하나 생기고, 누군가는 그것을 제거해야 합니다. 엔지니어 80명 규모의 조직에서 수백 개의 오래된 플래그를 안고 가는 팀을 본 적이 있습니다. 각 플래그는 아무도 완전히 테스트하지 못하는 코드 분기입니다.

플래그는 계약상 수명이 짧아야 합니다. 생성할 때 각 플래그에 소유자와 만료일을 정하고, 만료된 플래그는 경고를 띄웁니다. 플래그를 제거하는 같은 PR에서 죽은 코드 경로도 삭제합니다.

조합의 위험도 실제입니다. 독립된 불리언 플래그 10개는 1,024가지 설정을 만듭니다. 그중 전부를 테스트할 수는 없습니다. 플래그 간 상호작용은 적게 유지하고, 릴리스 플래그는 킬 스위치 같은 장기 운영 토글과 분리하세요.

트렁크 위에서 릴리스하는 두 가지 전략

트렁크에서 릴리스를 자르는 방법은 두 가지가 흔하며, 둘 다 장기 브랜치를 요구하지 않습니다.

어느 쪽을 고를지는 탐지 시간에 달려 있습니다. 나쁜 배포를 5분 안에 알아채고 2분 안에 되돌릴 수 있다면 앞으로 고치면 됩니다. 평균 탐지 시간이 한 시간이라면 릴리스 브랜치가 발을 디딜 자리를 줍니다.

짧은 측선들이 하나의 본선으로 합쳐지는 철도 그림

관측성이 이 거래의 나머지 절반입니다

트렁크 기반 개발은 배포 빈도를 올리고, 그만큼 무언가 잘못될 순간도 늘립니다. 이 모델은 머지 후 몇 분 안에 회귀를 볼 수 있을 때만 작동합니다. 특정 릴리스에 묶인 에러율, 지연 시간 백분위수, 포화도가 필요합니다. 월요일에 누군가 들여다보는 대시보드로는 부족합니다.

유용한 테스트가 하나 있습니다. 머지 후 "이 변경이 p99나 에러율을 움직였나?"라는 질문에 탭 다섯 개를 열지 않고 답할 수 있나요? 답할 수 없다면, 하루에 열 번 머지할 준비가 아직 안 된 것입니다.

어떤 스택을 쓰든 요구사항은 같습니다. 그래프에 배포 마커를 찍고, SLO 번 레이트 알림을 설정하고, 지친 사람이 새벽 3시에도 생각 없이 실행할 수 있는 되돌리기 경로를 둡니다.

트렁크 기반 개발 vs GitFlow vs GitHub Flow

셋은 자주 섞입니다. 하나의 속성으로 구분하세요. 작업이 공유 브랜치를 떠나 있는 시간이 얼마나 긴가입니다.

이미 수명이 이틀 미만인 브랜치로 GitHub Flow를 쓰고 있다면, 생각보다 트렁크에 가까운 것입니다. 차이는 보통 도구가 아니라 플래그 관리의 엄격함과 리뷰 지연에 있습니다.

어떤 DORA 지표가 가장 먼저 움직이나

시작하기 전에 이 지표들을 계측하세요. 그러지 않으면 나중에 감으로 논쟁하게 됩니다. 배포 빈도와 변경 리드 타임이 가장 먼저 반응합니다. 배치가 작아질수록 파이프라인을 더 빨리 통과하기 때문입니다. 변경 실패율과 복구 시간은 그다음이며, 브랜칭 자체보다 관측성과 되돌리기 경로에 달려 있습니다.

숫자를 개인에 대한 성적표로 쓰지 마세요. 숫자는 시스템을 설명합니다. 리드 타임이 늘면 사람부터 탓하지 말고 리뷰 대기열과 CI 소요 시간을 먼저 보세요.

트렁크 기반 개발이 틀린 선택일 때

몇 가지 경우에는 도입을 건너뛰거나 적어도 미뤄야 합니다. 어느 경우에 해당하는지 솔직하게 보세요.

트렁크 기반 개발이 무언가를 보장해 주지도 않습니다. DORA의 발견은 2016년과 2017년 데이터에서 나온 상관관계이며, 이 관행을 따르는 팀이 더 나은 전달 및 운영 성과를 보였다는 것입니다. 브랜치 이름을 바꾼다고 파이프라인이 고쳐지지는 않습니다.

빅뱅 마이그레이션 없이 시작하는 방법

정책부터 발표하지 마세요. 먼저 측정하고, 그다음 줄이세요.

  1. 현재 브랜치 수명과 중간값 풀 리퀘스트 크기를 기록합니다. 이것이 기준선입니다.

  2. 상한을 설정합니다. 예를 들어 이틀 넘은 브랜치는 없도록 하고, 대시보드에 보이게 합니다.

  3. CI에서 가장 느린 부분을 고칩니다. 빌드가 30분 걸리면 다른 것은 아직 중요하지 않습니다.

  4. 실제 기능 하나에 플래그를 하나 도입합니다. 꺼진 상태로 배포하고, 한 스프린트 안에 제거합니다.

  5. 코드 동결은 마지막에 없앱니다. 되돌리기 경로가 실제로 한 번 쓰여 본 뒤에 말입니다.

한 달 뒤에 다시 검토하세요. 가장 먼저 움직이는 숫자는 보통 풀 리퀘스트 크기와 머지까지의 시간입니다. 변경 실패율은 더 오래 걸리며, 때로는 문제가 더 일찍 드러나기 때문에 개선되기 전에 잠깐 나빠지기도 합니다.

여러분의 포스트모텀은 브랜칭 모델에 대해 뭐라고 할까요?

배포 전에 던질 유용한 질문은 이것입니다. 포스트모텀에는 뭐라고 쓰일까요? 마지막 장애가 아무도 리뷰할 수 없었던 머지, 3주간 갈라져 있던 브랜치, 40개 변경을 묶은 릴리스에서 시작되었다면, 여러분은 이미 빚을 어디서 갚아야 하는지 알고 있는 것입니다.

지난 장애 세 건을 보세요. 몇 건이 크고 늦은 통합에서 비롯되었나요? 그 개수가 바로 여러분의 비즈니스 케이스입니다.

자주 묻는 질문

트렁크 기반 개발과 GitFlow의 가장 큰 차이는 무엇인가요?
브랜치 수명입니다. GitFlow는 기능 브랜치를 몇 주 동안 격리할 수 있지만, 트렁크 기반 개발은 브랜치 수명을 몇 시간 단위로 제한합니다.
정말 하루에 한 번은 트렁크에 머지해야 하나요?
DORA의 역량 페이지는 트렁크로의 머지를 하루에 최소 한 번으로 제시합니다. 이 숫자는 취향이 아니라 피드백 루프의 예산을 뜻합니다.
완성되지 않은 기능은 어떻게 배포하지 않고 머지하나요?
기능 플래그를 사용합니다. 코드는 꺼진 상태로 프로덕션에 배포되고, 내부 사용자부터 점진적으로 켜면서 각 단계마다 SLO 게이트를 둡니다.
피처 플래그는 언제 제거해야 하나요?
생성할 때 소유자와 만료일을 정합니다. 만료된 플래그는 경고를 띄우고, 플래그를 제거하는 같은 PR에서 죽은 코드 경로도 삭제합니다.
CI는 얼마나 빨라야 하나요?
약 10분 이내를 목표로 하는 것이 좋습니다. CI가 40분 걸리면 개발자들이 변경을 묶어서 올리게 되고, 그러면 모델이 무너집니다.
도입 후 어떤 지표부터 확인해야 하나요?
배포 빈도와 변경 리드 타임이 가장 먼저 움직입니다. 변경 실패율과 복구 시간은 관측성과 되돌리기 경로에 달려 있어 더 늦게 개선됩니다.