cd ../
DevOps·2026-04-20·5 min read·# entry/035

Merge 커밋은 왜 생기고, 왜 복잡해지는가

충돌이 없는데도 Merge 커밋이 생기는 이유, 3-way Merge 알고리즘, 그리고 복잡해진 Git 그래프를 정리하는 전략까지.

어떤 때는 Merge가 커밋 없이 조용히 끝나고, 어떤 때는 Merge Commit이 하나 생겨납니다. 충돌도 없는데 커밋이 생기는 게 히스토리를 지저분하게 만드는 느낌이 들기도 합니다.

모든 Merge가 커밋을 만드는 건 아니다. 그렇다면 어떤 조건에서 커밋이 생기는가?

이를 이해하려면 Git이 데이터를 관리하는 구조인 DAG(Directed Acyclic Graph)3-way Merge 알고리즘을 살펴봐야 합니다.


Merge 커밋이 생기는 조건

Git에서 모든 Merge가 커밋을 생성하지는 않습니다. 커밋 히스토리가 한 줄로 이어질 수 있는 상황이라면 브랜치 포인터만 앞으로 이동시킵니다. 이를 Fast-Forward라고 합니다.

Fast-Forward (커밋 없음)
  main:    A → B
  feature:       → C → D

  머지 결과: main이 D를 가리키도록 포인터만 이동
  A → B → C → D  (선형 유지)

Merge 커밋이 생기는 상황은 두 가지입니다.

  • 공통 조상(Base) 이후로 두 브랜치가 서로 다른 방향으로 진행되었을 때
  • --no-ff 옵션으로 Fast-Forward 가능한 상황에서도 명시적으로 머지 커밋을 남기고자 할 때
Merge 커밋이 생기는 경우
  main:    A → B → C
  feature:     ↘ D → E

  공통 조상 B 이후 두 브랜치가 갈라짐 → Merge 커밋 M 생성
  A → B → C → M
          ↗
      D → E

3-way Merge 알고리즘

Merge 커밋을 생성할 때 Git은 단순히 두 브랜치의 끝만 비교하지 않습니다. 세 가지 지점을 참조합니다.

  1. 공통 조상 (Base): 두 브랜치가 갈라지기 직전의 마지막 커밋
  2. HEAD: 현재 작업 중인 브랜치의 최신 커밋
  3. Target: 합치려는 브랜치의 최신 커밋
왜 2-way가 아닌 3-way인가

두 브랜치의 최신 커밋만 비교(2-way)하면 어떤 쪽이 "원래 상태"이고 어떤 쪽이 "변경한 것"인지 알 수 없습니다. 공통 조상을 기준점으로 삼아야 각 브랜치가 무엇을 의도적으로 바꿨는지 정확히 파악할 수 있습니다.

Git은 공통 조상 대비 각 브랜치에서 어떤 변경이 있었는지 줄 단위로 비교합니다.

상황결과
조상 대비 Branch 1만 변경Branch 1의 내용 채택
조상 대비 Branch 2만 변경Branch 2의 내용 채택
두 브랜치 모두 같은 내용으로 변경해당 내용 채택
두 브랜치가 같은 부분을 다르게 수정Conflict — 사용자 개입 필요

이 비교 과정을 통해 Git은 모든 변경사항을 통합한 새로운 스냅샷을 만듭니다.


Merge 커밋의 내부 구조

일반 커밋은 바로 이전 커밋 하나만을 부모로 갖습니다. Merge 커밋의 가장 큰 특징은 부모가 2개 이상이라는 점입니다.

일반 커밋:    A ← B ← C
                       ↑
                   parent: 1개

Merge 커밋:   A ← B ← C ← M
                  ↑         ↑
              D ← E ←───────┘
                        parent: 2개 (C, E)
  • 포인터의 결합: 두 개의 부모 포인터를 가짐으로써 분리되었던 히스토리 두 줄이 하나로 합쳐졌음을 그래프에 명시합니다.
  • 새로운 스냅샷: Merge 결과물인 파일 시스템 상태를 tree 객체로 기록합니다.

Merge 커밋 방식의 이점

Merge 커밋을 남겼을 때
  • 맥락 유지: 언제 어떤 브랜치가 병합되었는지 이벤트로 기록 - 충돌 최소화: 공통 조상 기준 비교로 단순 diff보다 의도 파악이 정확 - 유연한 복구: 머지 커밋 하나를 revert하면 해당 기능 전체를 되돌릴 수 있음
Merge 커밋이 쌓이면
  • 히스토리가 비선형이 되어 추적이 어려워짐 - git bisect로 버그를 찾을 때 노드가 많아 특정이 까다로워짐 - 역방향 머지가 반복되면 그래프가 급격히 복잡해짐

복잡해지는 Git 그래프

Merge 커밋이 누적되면 아래와 같은 "기찻길 현상"이 생깁니다.

복잡하게 얽힌 Git 히스토리 그래프

이 현상은 단순히 머지를 많이 해서가 아니라, 비선형 히스토리 관리 전략이 반복될 때 나타납니다.

왜 이런 형태가 만들어지는가

  • 두 부모의 결합: 머지 커밋은 두 개의 부모를 갖습니다. 이 두 지점을 연결하는 선이 그래프에 추가되면서 갈래가 생깁니다.
  • 지속적인 교차 병합: developrelease/prod 방향으로 주기적으로 머지하면 각 시점마다 연결선이 추가됩니다. 여기에 release/proddevelop 역방향 머지까지 섞이면 선들이 서로 꼬이기 시작합니다.
  • Fast-Forward 미사용: 한쪽 브랜치만 업데이트된 상황에서도 --no-ff를 사용하면 불필요한 머지 커밋이 계속 쌓입니다.
복잡한 그래프가 초래하는 문제
  • 가독성 저하: 특정 기능이 어떤 커밋들로 구성되었는지 추적하기 어렵습니다.
  • Conflict 반복: 공통 조상이 너무 멀어지면 머지할 때마다 동일한 충돌을 반복 해결해야 합니다.
  • Bisect 난항: git bisect로 버그 커밋을 찾을 때 머지 노드가 많으면 이진 탐색이 꼬입니다.

히스토리를 정리하는 전략

내 브랜치의 변경사항을 대상 브랜치의 최신 커밋 위로 재배치합니다.

git checkout feature
git rebase main
git checkout main
git merge feature  # 이제 Fast-Forward 가능
Before:  A → B → C  (main)
             ↘ D → E  (feature)

After:   A → B → C → D' → E'  (선형 유지)
  • 모든 커밋이 한 줄로 이어지며 머지 커밋이 남지 않습니다.
  • 커밋 해시가 재작성되므로, 이미 원격에 push된 커밋을 Rebase하면 충돌이 발생할 수 있습니다.
공유 브랜치에서의 Rebase는 주의

main, develop 같이 다른 사람과 공유하는 브랜치에서는 Rebase를 사용하지 않습니다. 혼자 작업하는 feature 브랜치에서만 사용하는 것이 안전합니다.

어떤 전략을 선택할까

팀의 워크플로우와 브랜치 전략에 따라 다릅니다.

  • 맥락 보존이 중요한 팀: Merge 커밋 유지 + --no-ff 일관 적용
  • 히스토리 가독성이 우선인 팀: feature 브랜치는 Squash, 장기 브랜치는 Rebase
  • 오픈소스 / PR 기반 팀: Squash and Merge로 PR 단위 히스토리 유지