병합
갈라진 둘 이상을 하나로 되돌리는 일입니다. 각자 따로 고친 것을 한 벌로 모읍니다. 서로 다른 자리를 고쳤으면 기계가 알아서 합칩니다. 같은 자리를 서로 다르게 고쳤으면 사람이 골라야 합니다.
상세
같은 원고를 두 사람이 각자 복사해 가서 빨간 펜으로 고쳐 왔다고 해 봅시다. 서로 다른 문단을 고쳤다면 두 벌을 한 벌로 옮겨 적는 데 걸릴 것이 없습니다. 같은 문단을 서로 다르게 고쳤다면 누군가 하나를 골라야 합니다.
병합은 갈라진 둘 이상을 하나로 되돌리는 일입니다. 재료는 대개 셋입니다. 갈라지기 전의 공통 조상, 그 뒤 이쪽이 고친 것, 그리고 저쪽이 고친 것입니다. 조상을 기준으로 양쪽이 무엇을 바꿨는지 각각 뽑아내면, 두 벌의 변경을 한 벌 위에 함께 얹는 일이 됩니다.
한쪽만 건드린 자리는 그쪽 것을 그대로 씁니다. 양쪽이 다 건드렸어도 같은 방향으로 고쳤으면 역시 한 벌로 정해집니다. 남는 것은 양쪽이 같은 자리를 서로 다르게 고친 경우입니다. 사람은 두 수정이 결국 같은 말인지 읽어서 판단합니다. 병합은 그 판단을 못 합니다. 양쪽이 같은 자리를 건드렸다는 것까지만 압니다. 그래서 어느 쪽이 맞는지 정할 근거가 절차 안에 없습니다. 그 자리를 충돌이라고 부릅니다. 병합은 거기서 멈춰 사람에게 넘깁니다.
flowchart TD
A[공통 조상] --> B[이쪽 갈래]
A --> C[저쪽 갈래]
B --> D{같은 자리를 서로 다르게 고쳤나}
C --> D
D -->|아니오| E[합친 한 벌]
D -->|예| F[충돌 · 사람이 고른다]
그래서 병합의 결과는 둘 중 하나입니다. 전부 자동으로 정해져 합쳐진 한 벌이 나오거나, 정하지 못한 자리를 남긴 채 멈춥니다. 갈라진 것이 애초에 없을 때도 있습니다. 한쪽이 다른 쪽이 이미 지나온 자리에 그대로 얹혀 있는 경우입니다. 견줄 것이 없어서 합치는 일 자체가 생략됩니다.
배경
여럿이 같은 것을 동시에 고치려 하면 순서를 정해야 합니다. 한 번에 한 사람만 손대게 하면 나머지는 기다립니다. 기다리지 않으려면 각자 사본을 들고 따로 고쳐야 합니다. 그 순간 하나였던 것이 여러 벌로 갈라집니다.
갈라진 채로 두면 어느 것이 진짜인지 정해지지 않습니다. 그래서 갈라지는 것을 허용하려면 되돌릴 방법이 먼저 있어야 합니다. 갈라진 뒤에 서로 무엇을 고쳤는지 견주는 절차가 그것입니다. 겹치지 않는 것은 그대로 살립니다. 겹치는 것만 사람에게 물어봅니다.
그 절차를 병합이라고 부릅니다. 갈라진 갈래를 도로 하나로 모은다는 뜻 그대로입니다. 병합이 있어야 갈라지는 것이 사고가 아니라 선택이 됩니다.
예시
패스트 포워드 출력
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
합쳐 넣은 브랜치가 가리키던 커밋이 현재 커밋의 바로 앞에 있었습니다. Pro Git 은 첫 커밋의 이력을 따라가면 닿을 수 있는 커밋과 합치려 할 때는 합칠 갈라진 작업이 없다고 적습니다. 그래서 포인터를 앞으로 옮기는 것으로 끝납니다. 이것을 패스트 포워드라고 부릅니다. 새 커밋은 만들어지지 않았습니다. 출력에 나온 것은 무엇이 어디까지 갱신됐는지뿐입니다.
병합 커밋 출력
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)
이번에는 현재 브랜치의 커밋이 합쳐 넣을 브랜치의 직계 조상이 아니었습니다. Pro Git 은 이때 git 이 두 브랜치 끝이 가리키는 스냅샷과 둘의 공통 조상, 이렇게 셋을 써서 세 갈래 병합을 한다고 적습니다. 결과로 새 스냅샷이 생깁니다. 그것을 가리키는 커밋이 자동으로 만들어집니다. 이 커밋을 병합 커밋이라고 부릅니다. 부모가 하나보다 많다는 점이 특별합니다. 출력에 찍힌 recursive 는 전략 이름입니다. git 매뉴얼은 recursive 가 지금 ort(Ostensibly Recursive's Twin) 의 동의어라고 적습니다. v2.50.0 에서 ort 를 가리키도록 바뀌었습니다.
매뉴얼은 갈라졌다 합쳐지는 모양을 이렇게 그립니다.
flowchart TD
E["E · 갈라진 자리"] --> F --> G["G · master 끝"] --> H["H · 병합 커밋"]
E --> A --> B --> C["C · topic 끝"] --> H
master 는 E 에서 갈라진 뒤 F 와 G 로 갔습니다. topic 은 같은 자리에서 A·B·C 로 갔습니다. 매뉴얼은 병합 결과를 새 커밋에 기록할 때 두 부모 커밋의 이름과 사용자가 쓴 로그 메시지를 함께 적는다고 설명합니다. 그림의 H 가 그 커밋이고, 작업 전에 ORIG_HEAD 는 현재 브랜치 끝인 G 로 맞춰집니다.
충돌 파일
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
자동으로 정하지 못한 자리가 있어서 병합 커밋이 만들어지지 않고 절차가 멈췄습니다. 충돌한 파일에는 표시가 들어갑니다.
<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
please contact us at [email protected]
</div>
>>>>>>> iss53:index.html
======= 위쪽이 현재 체크아웃해 둔 쪽인 HEAD 의 내용입니다. 아래쪽이 합쳐 넣는 브랜치의
내용입니다. 매뉴얼은 앞쪽이 대개 이쪽이고 뒤쪽이 대개 저쪽이라고 적습니다. 해소하려면 한쪽을
고르거나 둘을 직접 합쳐 씁니다. 그런 다음 파일마다 git add 를 실행해 해소했다고 표시합니다.
이 동안 인덱스에는 충돌한 경로마다 최대 세 판이 올라가 있습니다. 1단은 공통 조상, 2단은 HEAD,
3단은 MERGE_HEAD 쪽 판입니다. 복잡한 충돌이 나와서 처음부터 다시 하고 싶으면
git merge --abort 로 되돌립니다.
갈래
축은 셋입니다. 무엇을 재료로 견주느냐, 합친 결과를 이력에 어떻게 남기느냐, 그리고 어느 자리에서 합치느냐입니다.
패스트 포워드
현재 브랜치 끝이 합쳐 넣을 커밋의 조상인 경우입니다. 매뉴얼은 이것이 가장 흔한 경우라고
적습니다. 특히 git pull 로 부를 때 그렇습니다. 합쳐진 이력을 담을 새 커밋이 필요 없습니다.
HEAD 와 인덱스를 그 커밋으로 옮기기만 합니다. 병합 커밋은 따로 만들지 않습니다.
이 동작은 옵션으로 조절합니다.
| 옵션 | 동작 |
|---|---|
--ff (기본값) |
가능하면 패스트 포워드로 해소합니다. 안 되면 병합 커밋을 만듭니다 |
--no-ff |
패스트 포워드로 될 때에도 언제나 병합 커밋을 만듭니다 |
--ff-only |
패스트 포워드로 안 되면 병합을 거절합니다. 0 이 아닌 상태로 끝납니다 |
세 갈래 병합
패스트 포워드가 아니면 두 브랜치는 둘 다를 부모로 갖는 병합 커밋으로 묶여야 합니다. 기본 전략은 ort 입니다. 브랜치 끝 둘을 세 갈래 병합 알고리즘으로 해소합니다. 쓸 수 있는 공통 조상이 둘 이상이면 조상들을 먼저 합쳐 만든 트리를 세 갈래 병합의 기준 트리로 삼습니다. 이름은 "Ostensibly Recursive's Twin" 의 머리글자입니다. 이전 기본 전략이던 recursive 를 대신하려고 쓰였기 때문에 그렇게 붙었습니다.
같은 자리에 다른 전략도 있습니다. resolve 는 브랜치 끝 둘만 세 갈래 병합으로 해소합니다. 십자 병합의 모호함을 조심스럽게 탐지하려 합니다. 이름 변경은 다루지 않습니다. ours 는 브랜치 끝을 몇 개든 해소하지만 결과 트리가 언제나 현재 브랜치의 것이라 다른 브랜치의 변경을 전부 무시합니다. 곁가지의 낡은 개발 이력을 대체하는 용도입니다. subtree 는 ort 를 고친 것입니다. 합칠 트리 B 가 A 의 하위 트리에 해당하면 B 를 A 의 트리 구조에 맞춰 조정한 다음 합칩니다.
옥토퍼스
브랜치 끝이 둘보다 많은 경우를 해소합니다. 대신 사람 손이 필요한 복잡한 병합은 거절합니다. 주로 토픽 브랜치 끝들을 한 번에 묶는 용도입니다. 브랜치를 둘보다 많이 부를 때의 기본 전략입니다.
스쿼시
여기서부터는 결과를 어떻게 남기느냐 쪽 축입니다. --squash 는 실제 병합이 일어난 것처럼 작업
트리와 인덱스 상태를 만듭니다. 다만 커밋을 만들지도 HEAD 를 옮기지도 MERGE_HEAD 를 기록하지도
않습니다. 그래서 다음 커밋이 병합 커밋이 되지 않습니다. 다른 브랜치를 합친 것과 같은 효과를 내는
커밋 하나가 현재 브랜치 위에 얹힙니다. --squash 와 --commit 은 같이 쓸 수 없습니다.
설정 병합
여기서부터는 어느 자리에서 합치느냐 쪽 축입니다. 버전관리 밖에도 같은 구조가 있습니다.
쿠버네티스의 kubectl apply 는 설정 파일과 라이브 설정, 그리고 라이브 쪽에 저장된
last-applied-configuration 애노테이션 셋을 놓고 API(Application Programming Interface)
서버로 보낼 패치를 계산합니다. 지울 필드는 애노테이션에 있으면서 설정 파일에 없는 것들입니다.
넣거나 바꿀 필드는 설정 파일에 있으면서 라이브 값과 다른 것들입니다.
필드 종류마다 합치는 방식이 다릅니다.
| 필드 종류 | 합치는 방식 |
|---|---|
| 원시 — 문자열 · 정수 · 불리언 | 대체합니다 |
| 맵 | 원소나 하위 필드를 합칩니다 |
| 리스트 | 경우에 따라 다릅니다 |
자동 수렴
분산 시스템에서는 갈라진 상태를 합치는 일이 분할 복구 절차가 됩니다. 에릭 브루어는 CAP(Consistency · Availability · Partition tolerance) 을 열두 해 뒤에 다시 짚은 글에서 이 일을 다룹니다. 분할 시점의 상태에서 출발해 양쪽 연산을 어떤 방식으로든 앞으로 굴리는 편이 대체로 더 수월하다고 적습니다. 굴리는 동안 일관된 상태를 유지해 갑니다. 소스 코드 관리 시스템인 CVS(Concurrent Versioning System)가 공유된 일관 지점에서 출발해 갱신을 굴려 브랜치를 합치는 것과 같은 방식입니다.
다만 대부분의 시스템이 충돌을 언제나 합칠 수 있는 것은 아닙니다. CVS 도 때때로 사람이 직접 풀어야 하는 충돌을 냅니다. 오프라인 모드가 있는 위키 시스템은 대개 문서에 충돌을 남겨 손으로 고치게 합니다. 반대로 쓸 연산을 정해 두면 언제나 합칠 수 있습니다. 구글 독스의 텍스트 편집은 연산을 스타일 적용과 텍스트 추가·삭제로 한정합니다. 브루어는 충돌 해소라는 일반 문제가 풀리지는 않는다고 적습니다. 다만 설계자가 분할 중에 쓸 연산을 제한하기로 고르면 복구 때 시스템이 상태를 자동으로 합칠 수 있습니다. 불변식을 되살리는 방법으로는 일부 갱신을 무시하는 "마지막에 쓴 쪽이 이긴다" 같은 단순한 것, 연산을 합치는 더 영리한 방식, 사람에게 올리는 것을 듭니다.
관련 항목
병합이 견주는 재료
병합이 다루는 저장소 상태
HEAD · MERGE_HEAD · ORIG_HEAD · 인덱스 · 작업 트리
병합이 맺는 결과
병합이 나뉘는 방식과 그 대안
세 갈래 병합 · 패스트 포워드 · 스쿼시 · 옥토퍼스 · 자동 수렴 · 리베이스
병합이라는 뜻이 걸쳐 있는 맥락
버전관리 · 분할 복구 · 선언형 설정 관리 · CAP · 병합 정렬
병합을 구현한 도구와 그 산출물
git · kubectl apply · CVS · 패치
다른 이름: merge · 머지