이번에 저 맥북 샀습니다.

사실 맥북 사고 첫 게시물은 아니고 두 번째 게시물인데
갑자기 개발블로그긴 하지만, 아무튼 블로그기도 하니까 뭔가 일상토크도 하고 싶어져서 투박하게 자랑해봅니다 ...

맥북사고 옆에 패드 연결하고 막 세팅하니까
벌써 무슨 짬 1년은 된 개발자 느낌 물씬 나고 막 괜히 기분 좋게 개발 중입니다 ...

아무튼 Leshgo


1. 왜 Merge 방식은 여러 개인가?

"합치는 행위" < "남기는 방식"

혼자 개발할 때의 merge는 그저 "코드 합치기"에 불과하다.
하지만 협업에서의 merge는 작업 흐름을 기록하는 행위가 된다.

  • 누가 어떤 기능을 언제 어디서 분기해서 작업했는지
  • 어떤 PR/이슈 단위로 들어갔는지
  • 나중에 문제가 생겼을 때 어디로 돌아가야 하는지

이걸 커밋 히스토리로 읽어야 하기 때문이다.

프로젝트가 원하는 히스토리 철학

"브랜치의 흔적이 남아야 한다" (추적/감사/회귀 용이)

  • 기능 단위로 작업 흐름이 보이면 디버깅, 회귀할 때 용이하다
  • 대규모 팀/장기 운영 프로젝트에서 선호한다

"히스토리는 최대한 한 줄로 깔끔해야 한다" (가독성/유지보수)

  • 커밋 로그가 복잡하면 리뷰/탐색 비용이 커진다
  • "기능 하나 = 커밋 하나" 같은 규칙을 선호하기도 한다

"불필요한 merge commit"은 싫다 (단순함)

  • 분기 없이 직선으로 진행된 경우라면 굳이 "합쳤다" 기록이 필요 없다

같은 코드여도, 달라지는 "다음 작업"

merge 방식은 코드 결과만 바꾸는 게 아니라, 이후 작업 방향에도 영향을 준다

  • 로그 탐색 난이도
  • git bisect 같은 디버깅 흐름
  • revert (되돌리기) 단위
  • 릴리즈/핫픽스 브랜치 운영
  • 리뷰 문화(PR 단위 유지 vs 커밋 단위 유지)

그래서 팀마다 "우린 이 모양으로 기록하자"와 같은 팀 컨벤션의 기준이 생기고, 그 선택지를 Git이 여러 방식으로 제공한다.


2. 대표적인 세 가지 Merge 방식: Fast-forward

히스토리 모양

처음 상태:

A --- B (main)
		\
         C --- D (feature)

여기서 main은 B에 멈춰 있고
feature는 C, D까지 진행된 상태다.

이때 main에서 feature를 merge하면?

결과:

A --- B --- C --- D (main)
  • merge commit 없음
  • 브랜치 흔적 없음
  • 그냥 포인터가 D로 이동

이러한 형태의 merge를 "Fast-forward merge"라고 한다.

히스토리 철학 관점

Fast-forward가 어울리는 철학은:

  • 히스토리는 직선이 좋다.
  • 불필요한 merge commit은 만들지 않는다.
  • 단순함 유지

즉,

"불필요한 merge commit"은 싫다.

이는 앞서 살펴본 히스토리 철학 중
'단순함'을 우선하는 전략에 해당한다.

언제 발생하는가?

Fast-forward는 다음과 같은 조건에서 발생한다:

  • main 브랜치에서 feature를 분기함
  • main에는 추가 커밋이 없음
  • feature에서만 커밋이 쌓임
  • 이 상태에서 main으로 merge

반대로 이런 상황에서는 불가능하다.

A --- B --- E (main)
		\
       	 C --- D (feature)

main에도 E가 생겼을 경우엔
단순히 포인터만 이동할 순 없다.

왜냐면 앞서 살펴본 히스토리 모양과 달리
feature에는 E가 없고
main에는 C, D가 없기 때문이다.

이러한 경우 Fast-forward는 더 이상 가능하지 않다.
이제 우리는 새로운 merge commit이 생성되는 3-way merge를 살펴볼 차례다.


3. 대표적인 세 가지 Merge 방식: 3-way merge

Fast-forward는 한쪽 브랜치만 앞으로 나아간 경우에만 가능하다.
그렇다면 두 브랜치가 각자 다른 방향으로 나아갔다면 어떻게 될까?

포인터 이동이 멈추는 순간

A --- B --- E (main)
		\
         C --- D (feature)
  • main은 E까지 진행되었고
  • feature는 C, D까지 진행되었다.

이제 main에서 feature를 merge하려고 한다.

하지만 이번에는 Fast-forward가 불가능하다.

왜냐하면:

  • main에는 E가 있고
  • feature에는 C, D가 있기 때문이다.

단순히 포인터를 이동하면
한쪽의 변경 내용이 사라지게 된다.

Git의 선택: 새로운 커밋을 만든다

Fast-forward가 불가능한 순간,
Git은 새로운 merge commit을 생성한다.

Git은 두 브랜치가 갈라진 지점, 즉 공통 조상(merge base) 을 기준으로
각 브랜치에서 어떤 변경이 있었는지 비교한다.

  • 공통 조상 B
  • main의 최신 커밋 E
  • feature의 최신 커밋 D

이 세 지점을 비교하여
변경 내용을 합친 새로운 커밋을 만든다.

이러한 형태의 merge를 "3-way merge"라고 한다.


히스토리 철학 관점

앞서 서술했듯이 3-way는 merge commit을 만든다.

이 말은 특정 브랜치가 특정 시점에 다른 브랜치와 합쳐졌다는 사실
히스토리에 명시적으로 기록한다는 뜻이다.

따라서 3-way merge는 단순히 코드를 합치는 방식이 아니라,
브랜치의 흐름 자체를 보존하는 전략이다.

즉,

"브랜치의 흔적이 남아야 한다"

이는 앞서 살펴본 히스토리 철학 중
'추적/감사/회귀'를 우선하는 전략에 해당한다.

결과

		  C	--- D
		/		 \
A --- B --- E --- M

M은 부모가 두 개인 merge commit이다.

Fast-forward가 "조용히 앞으로 이동하는 방식"이었다면
3-way merge는 "합쳐졌다는 사실을 남기는 방식"이다.

Fast-forward를 먼저 내세웠는데,
그것이 merge의 본질에 가깝거나 중요하기 때문이 아니라
3-way merge를 더 명확히 이해하기 위한 발판이었기 때문이다.

그만큼 이번 포스팅의 중심은 Fast-forward도,
후에 서술할 Squash merge도 아니다.
3-way merge가 이번 글의 main dish다.


4. 대표적인 세 가지 Merge 방식: Squash merge

기본 상황을 부여해서 시작해보자

A --- B --- X (main)
		\
         C --- D --- E (feature)

feature에서 커밋이 3개 쌓였고
main에 커밋이 1개 쌓였다.

이제 main으로 합치려고 한다.

3-way 였다면?

Git은 merge commit을 만든다.

		  C --- D --- E
		/				\
A --- B --- X ----------- M
  • C, D, E 그대로 남는다
  • merge commit M 생성
  • 브랜치 구조 보존

Squash를 선택하면?

Git은 이렇게 한다:

  • C, D, E의 변경 내용을 모두 모은다
  • 새 커밋 S를 만든다
  • 그 커밋을 main 위에 붙인다
A --- B --- X --- S

여기서 중요하게 봐야 할 건:

  • C, D, E는 main 히스토리에 없다
  • merge commit도 없다
  • S는 새로 만들어진 커밋이다

기본 merge와의 차이점

3-way

  • 구조 보존
  • merge commit 생성
  • feature의 커밋 유지

Squash

  • 구조 제거
  • merge commit 없음
  • feature의 커밋 제거
  • 변경 결과만 반영

히스토리 철학 관점

"히스토리는 최대한 한 줄로 깔끔해야 한다"

앞서 여러 차이점을 서술했지만 단순히 보면
3-way는 "이 브랜치가 이 시점에 합쳐졌다"를 보고,
Squash는 "이 기능이 추가되었다"를 본다.

이는 기록 단위가 다르다는 걸 의미하는데
feature 브랜치 안에는 feat, fix, refactor 등등
수많은 커밋이 있을 수 있다.

Squash는 "그런 중간 과정이 main에 중요하지 않다"고 본다.

실제로 main을 배포 브랜치로 사용한다면,
개발 과정의 모든 세부 기록이 반드시 필요하다고 보긴 어렵다.
핵심 변경만 명확히 드러나는 것이 유지보수 관점에서 유리할 수 있다.

이리 하여 앞서 살펴본 히스토리 철학 중
'가독성/유지보수'를 우선하는 전략에 해당한다.


5. Merge vs Rebase

우선 소제목에서부터 알 수 있듯이
Rebase는 merge의 한 종류가 아니다.

Merge는 두 브랜치를 합치는 행동인 반면,
Rebase는 한 브랜치를 다른 브랜치 위로 재배치한다.

Rebase도 상황을 두고 이해해보자 !

A --- B --- X (main)
		\
         C --- D (feature)
  • main은 X까지 진행
  • feature는 C, D까지 진행

이제 feature를 main 기준으로 최신 상태로 맞추고 싶다.

Merge를 선택하면?

		 C --- D
       /		 \
A --- B --- X --- M
  • merge commit 생성
  • 브랜치 구조 유지
  • 히스토리는 가지 형태

Rebase를 선택하면?

Git은 이렇게 한다:

  • C를 X 위로 옮긴다.
  • D도 그 위에 다시 붙인다.

결과:

A --- B --- X --- C' --- D'

중요:

  • C'와 D'는 원래 C, D가 아니다.
  • 새로 만들어진 커밋이다.
  • 기존 브랜치 구조는 사라진다.

Merge vs Rebase

이제 이 파트의 main인 둘의 차이가 보이는가.
그렇다면 내가 어찌저찌 잘 서술했나보다.

Merge:

  • 구조 보존
  • merge commit 생성
  • 과거를 유지

Rebase:

  • 병렬 구조 제거
  • merge commit 없음
  • 커밋을 재작성

왜 Rebase가 위험하다고 할까?

Rebase는 커밋을 재작성한다.

  • 커밋 해시가 바뀜
  • 이미 공유된 브랜치를 rebase하면 충돌 가능
  • 협업 중엔 조심해야 함

그래서

로컬에서만 rebase
공유 브랜치에서는 merge

라는 원칙이 흔히 나온다.


6. 핵심 정리

  • 단순함을 우선하여 불필요한 merge commit은 남기지 않는 Fast-forward
  • 브랜치가 합쳐짐을 기록해 추적/감사/회귀를 가능하게 하는 3-way merge
  • 여러 커밋을 하나로 압축해 기능 단위로 기록하는 Squash merge
  • 기준점을 옮겨 커밋을 재작성하는 Rebase

0개의 댓글