이번에 저 맥북 샀습니다.
사실 맥북 사고 첫 게시물은 아니고 두 번째 게시물인데
갑자기 개발블로그긴 하지만, 아무튼 블로그기도 하니까 뭔가 일상토크도 하고 싶어져서 투박하게 자랑해봅니다 ...
맥북사고 옆에 패드 연결하고 막 세팅하니까
벌써 무슨 짬 1년은 된 개발자 느낌 물씬 나고 막 괜히 기분 좋게 개발 중입니다 ...
아무튼 Leshgo
혼자 개발할 때의 merge는 그저 "코드 합치기"에 불과하다.
하지만 협업에서의 merge는 작업 흐름을 기록하는 행위가 된다.
이걸 커밋 히스토리로 읽어야 하기 때문이다.
merge 방식은 코드 결과만 바꾸는 게 아니라, 이후 작업 방향에도 영향을 준다
git bisect 같은 디버깅 흐름그래서 팀마다 "우린 이 모양으로 기록하자"와 같은 팀 컨벤션의 기준이 생기고, 그 선택지를 Git이 여러 방식으로 제공한다.
처음 상태:
A --- B (main)
\
C --- D (feature)
여기서 main은 B에 멈춰 있고
feature는 C, D까지 진행된 상태다.
이때 main에서 feature를 merge하면?
결과:
A --- B --- C --- D (main)
이러한 형태의 merge를 "Fast-forward merge"라고 한다.
Fast-forward가 어울리는 철학은:
즉,
"불필요한 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를 살펴볼 차례다.
Fast-forward는 한쪽 브랜치만 앞으로 나아간 경우에만 가능하다.
그렇다면 두 브랜치가 각자 다른 방향으로 나아갔다면 어떻게 될까?
A --- B --- E (main)
\
C --- D (feature)
main은 E까지 진행되었고feature는 C, D까지 진행되었다.이제 main에서 feature를 merge하려고 한다.
하지만 이번에는 Fast-forward가 불가능하다.
왜냐하면:
main에는 E가 있고feature에는 C, D가 있기 때문이다.단순히 포인터를 이동하면
한쪽의 변경 내용이 사라지게 된다.
Fast-forward가 불가능한 순간,
Git은 새로운 merge commit을 생성한다.
Git은 두 브랜치가 갈라진 지점, 즉 공통 조상(merge base) 을 기준으로
각 브랜치에서 어떤 변경이 있었는지 비교한다.
이 세 지점을 비교하여
변경 내용을 합친 새로운 커밋을 만든다.
이러한 형태의 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다.
기본 상황을 부여해서 시작해보자
A --- B --- X (main)
\
C --- D --- E (feature)
feature에서 커밋이 3개 쌓였고
main에 커밋이 1개 쌓였다.
이제 main으로 합치려고 한다.
Git은 merge commit을 만든다.
C --- D --- E
/ \
A --- B --- X ----------- M
Git은 이렇게 한다:
A --- B --- X --- S
여기서 중요하게 봐야 할 건:
"히스토리는 최대한 한 줄로 깔끔해야 한다"
앞서 여러 차이점을 서술했지만 단순히 보면
3-way는 "이 브랜치가 이 시점에 합쳐졌다"를 보고,
Squash는 "이 기능이 추가되었다"를 본다.
이는 기록 단위가 다르다는 걸 의미하는데
feature 브랜치 안에는 feat, fix, refactor 등등
수많은 커밋이 있을 수 있다.
Squash는 "그런 중간 과정이 main에 중요하지 않다"고 본다.
실제로 main을 배포 브랜치로 사용한다면,
개발 과정의 모든 세부 기록이 반드시 필요하다고 보긴 어렵다.
핵심 변경만 명확히 드러나는 것이 유지보수 관점에서 유리할 수 있다.
이리 하여 앞서 살펴본 히스토리 철학 중
'가독성/유지보수'를 우선하는 전략에 해당한다.
우선 소제목에서부터 알 수 있듯이
Rebase는 merge의 한 종류가 아니다.
Merge는 두 브랜치를 합치는 행동인 반면,
Rebase는 한 브랜치를 다른 브랜치 위로 재배치한다.
Rebase도 상황을 두고 이해해보자 !
A --- B --- X (main)
\
C --- D (feature)
이제 feature를 main 기준으로 최신 상태로 맞추고 싶다.
C --- D
/ \
A --- B --- X --- M
Git은 이렇게 한다:
결과:
A --- B --- X --- C' --- D'
중요:
이제 이 파트의 main인 둘의 차이가 보이는가.
그렇다면 내가 어찌저찌 잘 서술했나보다.
Rebase는 커밋을 재작성한다.
그래서
로컬에서만 rebase
공유 브랜치에서는 merge
라는 원칙이 흔히 나온다.