병합 충돌
고친 사람 github-actions[bot]
병합 충돌은 따로 고친 코드를 하나로 합치다가 도구가 스스로 답을 못 내고 멈추는 일입니다. 두 사람이 같은 줄을 서로 다르게 고쳤을 때 일어납니다. 도구는 어느 쪽이 맞는지 모르므로 사람에게 고르게 합니다.
쉽고 빠른 이해
무슨 일인가 — 코드를 합치던 도구가 멈추고 사람에게 묻는 일입니다. 설정 파일의 timeout 값을 나는 30 으로, 동료는 60 으로 바꿨다면 둘을 합칠 때 도구가 멈춥니다.
왜 멈추나 — 도구가 아무 쪽이나 골랐다면 다른 사람이 고친 값이 알림 없이 사라졌을 겁니다. 멈춰서 묻는 것이 그 유실을 막는 방법입니다.
어떻게 도나
- 도구가 두 사람이 무엇을 바꿨는지 줄마다 견줍니다
- 한쪽만 고친 줄은 알아서 합칩니다. 둘이 다르게 고친 줄에서는 멈춥니다
- 사람이 남길 내용을 정해 주면 합치기가 이어집니다
대가 — 사람이 두 변경을 읽고 판단해야 해서 시간이 듭니다. 도구는 줄이 겹쳤는지만 보므로, 뜻이 어긋난 변경은 멈춤 없이 합쳐져 코드를 깨뜨리기도 합니다.
상세
이 절은 병합 충돌이 어떤 조건에서 나는지를 차례로 좁혀 갑니다. 앞쪽 소절들은 설정 파일 한 줄을 두 사람이 고치는 예로 설명합니다.
갈래로 나눠 고치고 다시 합치기
버전 관리 도구는 코드가 바뀐 이력을 남기는 도구입니다. 고친 내용은 한 묶음씩 이력에 쌓입니다. 이 한 묶음을 커밋이라고 부릅니다.
여러 사람이 한 코드를 동시에 고치면 서로의 작업이 뒤섞입니다. 그래서 이력을 갈래로 나눠 각자 자기 갈래에 커밋을 쌓습니다. 이 갈래가 브랜치입니다.
다 고친 브랜치는 다시 하나로 합칩니다. 이 일을 병합이라고 부릅니다. 병합은 대개 사람 손 없이 끝납니다. 도구가 양쪽 변경을 맞춰 보고 알아서 합칩니다.
병합 충돌은 이 자동 병합이 답을 못 내고 멈춘 상태입니다. 멈춘 파일은 사람이 직접 고쳐야 합니다. 그래야 병합이 이어집니다.
도구가 견주는 기준
두 브랜치의 최종본만 봐서는 누가 무엇을 바꿨는지 알 수 없습니다. 내 브랜치에는 timeout = 30 이, 동료 브랜치에는 timeout = 60 이 있다고 해 봅시다. 원래 값이 30 이었다면 동료만 고친 것입니다. 원래 값이 10 이었다면 둘 다 고친 것입니다.
그래서 도구는 두 브랜치가 갈라지기 직전의 커밋을 함께 꺼냅니다. 이 커밋을 공통 조상이라고 부릅니다. 공통 조상과 견주면 줄마다 누가 무엇을 바꿨는지 드러납니다.
flowchart TD
O["공통 조상 · timeout = 10"] --> A["내 브랜치 · timeout = 30"]
O --> B["동료 브랜치 · timeout = 60"]
A --> M["병합 · 양쪽이 다 고쳐서 멈춘다"]
B --> M
그림에서 두 브랜치는 같은 커밋에서 갈라져 각자 값을 바꿨습니다. 공통 조상의 10 과 견주면 양쪽 모두 고쳤다는 것이 보입니다. 이렇게 공통 조상과 두 브랜치, 세 벌을 함께 견주는 방식을 3방향 병합이라고 합니다.
줄마다 내리는 판정
도구는 공통 조상과 견줘 줄마다 네 경우 중 하나로 가릅니다. 아래 표는 경우마다 도구가 하는 일입니다.
| 누가 그 줄을 고쳤나 | 도구가 하는 일 |
|---|---|
| 아무도 안 고쳤다 | 원래 줄을 둔다 |
| 한쪽만 고쳤다 | 고친 쪽을 쓴다 |
| 양쪽이 똑같이 고쳤다 | 그 내용을 쓴다 |
| 양쪽이 서로 다르게 고쳤다 | 멈추고 사람에게 넘긴다 |
위의 세 경우는 남길 내용이 하나로 정해집니다. 마지막 경우만 후보가 둘입니다. 도구는 코드의 뜻을 모르므로 둘 중 무엇이 맞는지 가릴 수 없습니다.
이때 도구가 아무 쪽이나 골랐다면 다른 쪽 수정은 알림 없이 사라졌을 겁니다. 합친 사람도 고친 사람도 그걸 모른 채 넘어갑니다. 병합 충돌은 이 조용한 유실을 막으려고 도구가 일부러 멈추는 것입니다.
재현되는 조건
넷이 다 서면 병합 충돌이 납니다.
- 두 브랜치가 같은 커밋에서 갈라집니다
- 두 브랜치가 같은 파일의 같은 줄을 각각 고칩니다
- 고친 내용이 서로 다릅니다
- 한 브랜치를 다른 브랜치에 병합합니다
하나만 빠져도 충돌은 안 납니다. 서로 떨어진 줄을 고쳤다면 둘째가 빠진 것이라 도구가 두 변경을 다 받아들입니다. 셋째가 빠지면 남길 내용이 하나뿐이라 멈출 까닭이 없습니다.
같은 줄의 범위는 조금 넓게 봐야 합니다. 도구는 바뀐 줄들을 덩어리로 묶어 견줍니다. 도구에 따라서는 바로 맞붙은 두 줄을 따로 고친 것도 한 덩어리로 보고 충돌로 칩니다.
줄이 아니라 파일째 부딪히는 경우
줄을 견주기 전에 파일 단위로 부딪히기도 합니다. 한쪽은 파일을 지웠는데 다른 쪽은 그 파일을 고쳤다면 도구는 멈추고 사람에게 묻습니다. 양쪽이 같은 이름으로 서로 다른 파일을 새로 만들었을 때도 멈춥니다.
글자가 아닌 파일도 같은 일을 겪습니다. 이미지나 압축 파일처럼 바이트로 된 파일을 이진 파일이라고 합니다. 이진 파일에는 줄이라는 단위가 없습니다. 그래서 양쪽이 조금씩만 고쳐도 파일 전체가 한 덩어리로 충돌합니다.
아래 표는 파일째 부딪히는 세 경우를 모은 것입니다.
| 한쪽이 한 일 | 다른 쪽이 한 일 | 도구가 정하지 못하는 것 |
|---|---|---|
| 파일을 지웠다 | 그 파일을 고쳤다 | 고친 내용을 살릴지, 파일을 지울지 |
| 새 파일을 더했다 | 같은 이름으로 다른 파일을 더했다 | 그 이름에 두 파일 중 어느 쪽을 둘지 |
| 이진 파일을 고쳤다 | 같은 이진 파일을 고쳤다 | 어느 쪽 파일을 남길지 |
세 경우 모두 사람이 남길 파일을 정해 주면 병합이 이어집니다.
충돌이 난 파일의 모양
충돌이 나면 도구는 문제의 줄을 파일 안에 두 벌 다 적어 둡니다. 두 벌을 가르는 기호 줄을 충돌 마커라고 부릅니다. 아래는 내가 내 브랜치에서 동료의 feature 브랜치를 병합했을 때 Git 이 적어 두는 모양입니다.
<<<<<<< HEAD
timeout = 30
=======
timeout = 60
>>>>>>> feature
<<<<<<< 줄과 ======= 줄 사이가 지금 내가 있는 브랜치의 내용입니다. Git 은 지금 있는 브랜치를 HEAD 라는 이름으로 가리킵니다. ======= 줄과 >>>>>>> 줄 사이는 합쳐 들어오는 feature 브랜치의 내용입니다.
충돌이 안 난 줄은 이미 합쳐진 채로 파일에 들어 있습니다. 사람이 볼 곳은 마커로 둘러싸인 부분뿐입니다.
풀고 병합을 마치는 순서
멈춘 병합은 사람이 최종본을 정해 줘야 이어집니다. 이 일을 충돌 해소라고 부릅니다.
사람은 두 벌 중 하나를 고르거나 둘을 섞어 새로 씁니다. 그다음 마커 줄을 지우고 이 파일은 다 풀었다고 도구에 알립니다. 충돌 난 파일을 전부 알리면 도구가 병합을 마치고 병합 커밋을 만듭니다.
sequenceDiagram
participant 개발자
participant 도구
개발자->>도구: 동료 브랜치를 병합한다
Note over 도구: 한쪽만 고친 줄은 먼저 합쳐 둔다
도구-->>개발자: 충돌 난 파일을 알리고 멈춘다
개발자->>도구: 마커를 지운 파일을 다 풀었다고 알린다
개발자->>도구: 병합을 마친다
도구-->>개발자: 병합 커밋을 만든다
Git 에서는 다 풀었다고 알리는 명령이 git add 입니다. 병합을 마치는 명령은 git commit 입니다. 풀기를 그만두고 병합 전으로 돌아가려면 git merge --abort 를 씁니다.
마커 줄을 지우지 않고 알려도 Git 은 막지 않습니다. 그러면 <<<<<<< 줄이 코드에 섞여 들어갑니다. 이 실수는 빌드나 실행에서야 드러납니다.
병합 말고도 멈추는 명령
커밋을 다른 곳에 옮겨 얹는 명령도 속으로는 같은 3방향 병합 방식으로 견줍니다. 그래서 같은 조건에서 같은 모양으로 멈춥니다. 아래 표의 원격 저장소는 팀이 함께 쓰는 서버 쪽 저장소입니다.
| 명령 | 하는 일 |
|---|---|
| 리베이스 | 내 커밋들을 다른 브랜치 끝에 하나씩 다시 얹는다 |
| 체리픽 | 다른 브랜치의 커밋 하나만 골라 지금 브랜치에 얹는다 |
| 되돌리기 | 지난 커밋이 한 변경을 거꾸로 얹어 없던 일로 만든다 |
| 풀 | 원격 저장소의 변경을 받아와 지금 브랜치에 병합한다 |
리베이스는 커밋을 하나씩 얹으므로 커밋마다 멈출 수 있습니다. 같은 줄을 여러 커밋이 고쳤다면 비슷한 충돌을 여러 번 풀게 됩니다.
줄이 안 겹쳐도 깨지는 코드
도구가 보는 것은 글자가 어디서 겹쳤는지뿐입니다. 코드의 뜻이 어긋났는지는 보지 않습니다. 그래서 충돌 없이 끝난 병합이 깨진 코드를 낳기도 합니다.
한 사람은 함수 이름을 load 에서 fetch 로 바꿨습니다. 다른 사람은 다른 파일에 load 를 부르는 줄을 새로 더했습니다. 두 변경은 서로 다른 파일의 다른 줄이라 병합은 멈추지 않고 끝납니다.
# 내 브랜치 · 이름을 바꿨다
def fetch(user_id): ...
# 동료 브랜치 · 다른 파일에 더한 줄
load(42) # NameError
합친 코드를 돌리면 동료가 더한 줄이 이제 없는 load 를 부릅니다. 파이썬은 없는 이름을 부르면 NameError 를 냅니다. 이런 어긋남을 의미 충돌이라고 부릅니다. 줄 단위 충돌 검사로는 안 잡힙니다. 합친 뒤의 빌드나 테스트가 잡아야 합니다.
거꾸로 뜻은 안 부딪혀도 충돌이 나기도 합니다. 두 사람이 파일 맨 끝에 서로 다른 줄을 하나씩 더했다고 해 봅시다. 둘 다 남기면 끝나는 일인데도 도구는 멈춥니다.
새로 더한 줄도 같은 자리에 들어가면 도구는 같은 덩어리를 양쪽이 다르게 바꾼 것으로 봅니다. 도구는 줄이 어디에 들어갔는지만 봅니다. 두 줄을 다 남겨도 되는지는 모릅니다.
두 축을 나란히 놓으면 네 경우가 나옵니다.
| 뜻이 부딪힌다 | 뜻은 안 부딪힌다 | |
|---|---|---|
| 줄이 겹친다 | 병합 충돌 · 사람이 골라야 한다 | 병합 충돌 · 둘 다 남기면 풀린다 |
| 줄이 안 겹친다 | 멈춤 없이 합쳐져 코드가 깨진다 | 멈춤 없이 합쳐진다 |
병합 충돌은 윗줄 두 칸입니다. 뜻이 아니라 글자가 겹쳤는지로 정해집니다.
충돌이 잦아지는 조건
두 브랜치가 떨어져 있는 시간이 길수록 같은 줄을 서로 고칠 일이 늘어납니다. 떨어진 동안 쌓인 변경은 합칠 때 한꺼번에 부딪힙니다. 그래서 오래 산 브랜치일수록 충돌이 자주 납니다. 한 번 날 때도 크게 납니다.
시간 말고 파일의 성격도 충돌을 부릅니다. 아래 표는 충돌이 자주 몰리는 곳입니다.
| 충돌이 몰리는 곳 | 까닭 |
|---|---|
| 여럿이 한 줄씩 더하는 목록 파일 | 의존성 목록이나 라우팅 표처럼 모두가 같은 끝에 줄을 더한다 |
| 파일 전체를 건드리는 변경 | 코드 모양을 한꺼번에 다시 맞추는 [[코드 포매터 |
| 기능이 한데 몰린 큰 파일 | 서로 다른 일을 해도 같은 파일의 같은 부분을 건드리게 된다 |
| 이진 파일 | 줄로 나눌 수 없어 양쪽이 고치면 곧 충돌이다 |
충돌을 줄이는 작업 방식
줄이는 길은 둘입니다. 하나는 두 브랜치가 떨어져 있는 시간을 줄여 부딪힐 변경이 쌓이지 않게 하는 것입니다. 다른 하나는 부딪힐 줄 수 자체를 줄이는 것입니다. 아래 세 문단은 시간을 줄이는 방법입니다.
트렁크 기반 개발은 브랜치를 짧게 둡니다. 모두가 한 줄기에 자주 합치므로 한 번에 부딪히는 변경이 작습니다.
지속적 통합은 합칠 때마다 빌드와 테스트를 자동으로 돌립니다. 그래서 병합 충돌뿐 아니라 의미 충돌도 합친 직후에 드러납니다.
덜 끝난 코드는 기능 플래그로 꺼 둔 채 먼저 합치기도 합니다. 기능 플래그는 기능을 켜고 끄는 설정 스위치입니다. 꺼 두면 사용자에게 안 보이므로 브랜치를 오래 들고 있을 까닭이 사라집니다.
부딪힐 줄 수를 줄이는 방법도 있습니다. 파일 전체를 바꾸는 서식 변경은 기능 변경과 따로 합칩니다. 둘을 섞으면 고친 줄 수십 개 사이에 진짜 충돌이 묻힙니다.
관련 항목
병합 충돌로 멈출 수 있는 명령
병합 · 리베이스 · 체리픽 · 되돌리기 · 풀 · 스태시
병합 충돌을 가리고 푸는 데 쓰는 개념
충돌 해소 · 3방향 병합 · 공통 조상 · 충돌 마커 · 병합 커밋 · HEAD · 병합 도구 · 병합 전략
병합 충돌이 일어나는 버전 관리의 구성 요소
버전관리 · Git · 커밋 · 브랜치 · 원격 저장소 · 트렁크
병합 충돌을 부르는 브랜치와 파일
장수 브랜치 · 이진 파일 · 큰 이진 파일 · 코드 포매터 · 줄 끝 문자
병합 충돌을 줄이는 작업 방식
트렁크 기반 개발 · 지속적 통합 · 기능 플래그 · 풀 리퀘스트 · 브랜치 전략 · 코드 리뷰
줄이 안 겹쳐도 병합 뒤에 드러나는 어긋남
시맨틱 컨플릭트 · 빌드 깨짐 · 통합 지옥 · 빌드 · 테스트
병합 충돌을 낳는 협업 방식과 그 대안
복사-수정-병합 · 잠금-수정-해제 · 파일 잠금 · 체크아웃
병합 충돌과 헷갈리는 이웃
다른 이름: merge conflict · 머지 충돌 · 머지 컨플릭트