왜 "그린한 PR"이 메인을 깨뜨리는가

PR이 통과했다는 것은 그 시점의 메인과 병합한 결과가 통과했다는 뜻이다. 여러 PR이 동시에 그린 상태로 대기하다 순차 머지되면, 각 PR은 서로의 변경을 반영하지 않은 채 병합된다. A는 함수 시그니처를 바꾸고 B는 그 함수를 새로 호출하는 코드를 추가했다면, 둘 다 개별 CI는 통과하지만 합쳐진 메인은 깨진다. 이를 시맨틱 충돌(semantic conflict)이라 부르며, git의 텍스트 병합 충돌과 달리 조용히 통과한다.

팀 규모가 커지고 하루 머지가 수십 건을 넘어가면 이 확률은 무시할 수 없다. 메인이 레드가 되면 그 위에 브랜치를 딴 모든 사람이 영향을 받고, 원인 PR을 찾아 리버트하는 동안 배포 파이프라인 전체가 멈춘다.

머지 큐의 핵심 아이디어

머지 큐(merge queue)는 "머지 직전에, 큐에 앞선 변경들과 합친 상태로 다시 테스트한다"는 규칙을 강제한다. 개발자가 PR을 큐에 넣으면 시스템이 다음을 자동으로 수행한다.

  • 현재 메인 + 앞선 큐 항목들 + 내 PR을 합친 임시 브랜치 생성
  • 이 임시 브랜치에서 전체 CI 실행
  • 통과하면 메인에 머지, 실패하면 큐에서 제외하고 뒷 항목 재정렬

즉 "통과했으니 머지"가 아니라 "머지될 그 상태로 통과해야 머지"로 순서를 뒤집는다.

GitHub Merge Queue 설정

브랜치 보호 규칙에서 "Require merge queue"를 켜고, 큐 전용 워크플로를 merge_group 이벤트로 트리거한다.

name: merge-queue-ci
on:
  merge_group:            # 큐가 만든 임시 브랜치에서 실행
  pull_request:           # 일반 PR 검증도 유지

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm test

주의할 점은 브랜치 보호의 "required status checks"에 이 워크플로 잡 이름이 등록돼 있어야 한다는 것이다. 그렇지 않으면 큐가 검증 없이 통과시킨다.

배치(batch)로 처리량 올리기

PR을 하나씩 검증하면 안전하지만 느리다. CI가 20분이고 큐에 10개가 쌓이면 마지막 항목은 3시간을 기다린다. 그래서 여러 항목을 묶어 한 번에 테스트하는 배치(speculative batching)를 쓴다.

전략처리량실패 격리적합한 상황
단건(single)낮음정확CI가 짧거나 머지 빈도 낮음
고정 배치높음배치 통째 재시도실패율 낮은 안정된 팀
이진 분할 배치높음범인만 격리머지 빈도 높고 CI 김

배치 중 실패가 나면 배치 전체를 버리지 말고 이진 분할로 범인 PR만 찾아 제외하면 나머지는 살릴 수 있다.

def bisect_batch(prs, run_ci):
    """배치 CI 실패 시, 범인 PR만 찾아 반환"""
    if len(prs) == 1:
        return prs if not run_ci(prs) else []
    mid = len(prs) // 2
    left, right = prs[:mid], prs[mid:]
    culprits = []
    if not run_ci(left):
        culprits += bisect_batch(left, run_ci)
    if not run_ci(right):
        culprits += bisect_batch(right, run_ci)
    return culprits   # 이들만 큐에서 빼고 나머지는 머지 진행

플래키 테스트라는 함정

머지 큐는 "CI가 곧 진실"이라는 전제 위에서 돈다. 테스트가 무작위로 실패(플래키)하면 죄 없는 PR이 큐에서 튕기고, 재시도가 반복되며 큐가 정체된다. 도입 전에 플래키 테스트를 격리하거나 quarantine 처리해야 한다. 무분별한 자동 재시도는 진짜 회귀까지 숨기므로, 재시도는 횟수를 제한하고 플래키로 분류된 테스트에만 적용한다.

운영 시 챙길 것

  • CI 시간 단축: 큐 지연은 CI 길이에 비례한다. 캐시, 변경 영향 범위 기반 선택 실행, 병렬화에 먼저 투자한다.
  • 긴급 배포 경로: 장애 대응 리버트는 큐를 우회할 수 있는 예외 경로를 명시적으로 둔다.
  • 큐 관측성: 대기 시간, 실패율, 재시도 횟수를 대시보드로 본다. 지표가 나빠지면 배치 크기와 플래키 테스트부터 점검한다.
  • 작은 PR 문화: 큐는 큰 PR을 근본적으로 빠르게 만들지 못한다. PR을 잘게 쪼개는 습관이 큐 효율을 결정한다.

머지 큐는 마법이 아니라 "머지 순서를 직렬화하고 그 순서대로 실제 병합 상태를 검증하는" 규율이다. CI 신뢰도와 속도가 받쳐줄 때 메인의 그린을 실질적으로 지켜준다.