개요
이 글은 GitLab MR 충돌이 발생하는 대표적인 상황과 안전한 해결 절차를 정리한 운영 기록입니다. 같은 main에서 갈라진 두 개의 Merge Request가 동일 파일의 인접한 영역을 각각 수정하고 있을 때, 한쪽 MR이 먼저 병합되면 나머지 MR이 갑자기 병합 불가(blocked) 상태로 바뀝니다. 처음에는 문제없이 병합 가능하던 MR이라 원인이 바로 보이지 않는 경우가 많습니다.
이번 사례에서는 두 MR이 같은 설정 파일의 동일 구획에 각각 항목을 추가한 탓에, 먼저 병합된 MR로 인해 뒤에 남은 MR에서 충돌이 드러났습니다. 진단 과정에서 git merge-tree가 충돌을 과소보고하는 함정도 함께 확인했으므로, 재현·진단·해결·검증 순서로 기록합니다.
환경
- Self-managed GitLab (merge commit 방식, MR 파이프라인의 validate 단계 사용)
- Git 2.40+ (
git merge-tree --write-tree지원) - 대상 파일: 여러 항목을 하나의 목록/딕셔너리에 누적하는 설정 파일 1개 (본문에서는
config-registry.py로 일반화) - 브랜치:
feature/a,feature/b— 동일main커밋에서 분기
단계별 절차
1. 상황 이해: 형제 MR이 같은 영역을 수정
feature/a와 feature/b는 각각 별도 MR로 열려 있었고, 두 브랜치 모두 같은 설정 파일의 같은 구획(예: 목록의 특정 지점)에 서로 다른 항목을 추가했습니다. 두 MR이 열린 시점에는 각각 main에 대해 병합 가능한 상태였습니다.
feature/a가 먼저 병합되면 main이 갱신되고, 아직 열려 있던 feature/b는 그 갱신된 main 기준으로 다시 3-way 병합을 시도하게 됩니다. 이때 같은 위치에 대한 변경이 겹치면서 feature/b가 충돌·블록 상태가 됩니다.
2. 원인 진단
먼저 원격을 갱신하고 main이 그사이 얼마나 움직였는지, 남은 브랜치가 얼마나 뒤처졌는지 확인합니다.
git fetch origin --prune # 남은 브랜치가 main 대비 얼마나 뒤처졌는지(behind) 확인 git rev-list --left-right --count origin/main...origin/feature/b # 출력 예: 3 4 (main-only 3, branch-only 4)
여기서 중요한 함정이 있습니다. git merge-tree로 미리 충돌을 확인하면 “충돌 없음”으로 나오는데도, 실제 git merge에서는 충돌이 발생하는 경우가 있습니다. 따라서 진단은 실제 병합으로 검증해야 합니다.
# merge-tree는 충돌을 과소보고할 수 있음(참고용) git merge-tree --write-tree origin/main origin/feature/b | grep -i conflict || echo "clean?"
3. 최신 main 병합 및 충돌 해결
남은 브랜치를 최신 main과 다시 맞추기 위해 main을 브랜치로 병합합니다. force-push나 히스토리 재작성 없이 병합 커밋으로 해결하는 편이 안전합니다.
git switch feature/b git merge origin/main --no-edit # CONFLICT (content): Merge conflict in config-registry.py
두 브랜치가 모두 “항목 추가”만 했다면, 해결의 원칙은 양쪽 추가분을 모두 보존(union)하는 것입니다. 다만 Git이 공통 boilerplate 줄(닫는 괄호, 공통 키 등)을 기준으로 정렬하면서 서로 다른 항목이 조각조각 뒤섞이는 경우가 있어, 충돌 마커만 기계적으로 지우면 항목이 오염될 수 있습니다. 그래서 마커 제거 대신, main 버전을 기준으로 두고 남은 브랜치가 추가한 블록만 통째로 삽입하는 방식으로 재구성했습니다.
# main 버전(먼저 병합된 항목 포함)을 기준으로 확보 git show origin/main:config-registry.py > /tmp/base.py # 남은 브랜치가 추가한 블록만 추출해 base의 안정적 anchor 앞에 삽입 # → 스크립트로 블록을 잘라 붙이면 손으로 마커를 지우는 것보다 안전합니다
트러블슈팅
정상 병합 가능하던 MR이 갑자기 블록됨
현상: 생성 시점에는 병합 가능하던 MR이, 별다른 변경 없이 갑자기 “merge blocked / conflict” 상태가 됨.
원인: 같은 파일의 같은 구획을 수정한 형제 MR이 먼저 병합되어 main이 갱신됨. 남은 MR의 병합 기준(base)이 바뀌면서 충돌이 새로 발생.
해결: 남은 브랜치에 최신 main을 병합(또는 rebase)해 다시 최신 상태로 맞춘 뒤 충돌을 해결하고 push. 병합 커밋 방식이면 rebase·force-push 없이 처리할 수 있습니다.
merge-tree는 clean인데 실제 merge에서 충돌
현상: git merge-tree --write-tree로는 충돌이 안 보였는데 실제 git merge에서 충돌 발생.
원인: merge-tree의 사전 판정이 실제 3-way 병합과 항상 일치하지는 않아, 특정 정렬 상황에서 충돌을 과소보고할 수 있음.
해결: 사전 점검은 참고로만 쓰고, 실제 git merge 결과로 최종 판정합니다. 실제 병합이 충돌을 정확히 드러냅니다.
충돌 마커가 서로 다른 항목을 뒤섞음
현상: 충돌 구간에서 <<<<<<</=======/>>>>>>> 마커가 한 항목의 일부와 다른 항목의 일부를 뒤섞어 놓음.
원인: 두 브랜치가 같은 위치에 각각 완결된 블록을 추가했지만, Git이 공통 boilerplate 줄을 기준으로 hunk를 정렬하면서 블록 경계가 깨짐.
해결: 마커를 손으로 지우지 말고, 한쪽(예: main) 전체를 기준으로 두고 다른 쪽이 추가한 블록만 통째로 삽입해 재구성. 이후 반드시 자동 검증(구문 컴파일, 중복 키 검사, 기대 항목 존재 여부)으로 union이 정확한지 확인합니다.
결과 확인
재구성한 파일이 구조적으로 올바른지 먼저 검증하고, 그다음 브랜치가 실제로 충돌 없이 병합 가능한지 확인했습니다.
# 1) 구문/구조 검증 (예: Python 파일) python3 -m py_compile config-registry.py && echo "compile OK" # 2) 병합 커밋 후, 충돌 없음 + 최신 main 포함(up-to-date) 확인 git merge-tree --write-tree origin/main origin/feature/b | grep -i conflict \ && echo "conflict" || echo "clean" git merge-base --is-ancestor origin/main origin/feature/b \ && echo "up-to-date (not behind)"
충돌 마커가 0개이고, 재구성한 파일이 컴파일되며 양쪽 항목이 모두 남아 있고, merge-tree가 clean이면 MR 블록이 해소됩니다. 결국 GitLab MR 충돌의 핵심 예방책은, 같은 파일의 같은 구획을 여러 MR이 동시에 건드리지 않도록 작업을 분리하고, 형제 MR이 먼저 병합되면 남은 MR에 main을 다시 병합해 최신 상태로 유지하는 것입니다.
참고
관련 포스트:
- Migrating WordPress from a Standalone LAMP Server to Kubernetes — GitLab Container Registry·CI 파이프라인을 활용한 이관 기록
- Kubernetes MariaDB Failover with Rook Ceph — 운영 중 장애 진단·복구 절차 기록
- Migrating Ingress Controller: ingress-nginx EOL to Traefik — 단계별 이관과 트러블슈팅 사례
참고 문서: git-merge · git-merge-tree · GitLab: Merge conflicts