ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • fast-forward의 정체 — 어떤 날은 merge commit이 생기고 어떤 날은 안 생기는 이유
    IT 2026. 8. 6. 21:30
    fast-forward의 정체 — 어떤 날은 merge commit이 생기고 어떤 날은 안 생기는 이유

    git을 배우기 시작한 지인이 이상하다며 화면을 보여줬다. "어제 git merge를 했을 땐 'Merge branch ...'라는 커밋이 생겼는데, 오늘 똑같이 했더니 Fast-forward라고만 뜨고 커밋이 안 생겼어. 내가 뭘 잘못 눌렀나?" 잘못 누른 게 아니다. git이 상황을 보고 스스로 두 가지 다른 동작을 고른 것이다. 그리고 이 자동 선택의 이름이 fast-forward다.

    fast-forward는 merge를 배울 때 가장 먼저 만나는 갈림길인데, 대부분의 입문 자료가 "빨리감기입니다" 한 줄로 넘어간다. 이 글은 그 한 줄 뒤에 있는 조건 — 언제 매듭 커밋이 필요 없고, 언제 반드시 필요한가 — 를 그림으로 뜯어본다.

    merge commit이 필요 없는 유일한 상황

    merge commit은 갈라진 두 갈래를 봉합하는 매듭이다. 뒤집어 말하면 갈라진 적이 없으면 매듭도 필요 없다. feature 브랜치를 딴 뒤 main에 아무 커밋도 안 들어온 상황을 보자.

    diagram

    다이어그램 설명. "C2 — main이 여기 서 있음"에서 "F2 — feature가 여기 서 있음"까지가 한 줄로 쭉 이어져 있다. 갈래가 없다. main이 가진 모든 역사(C1, C2)를 feature도 이미 포함하고 있고, feature는 거기에 두 커밋을 더 얹었을 뿐이다. 이 관계를 git 용어로 "main이 feature의 조상(ancestor)"이라고 한다.

    이 상태에서 git merge feature를 하면 git은 생각한다 — "합칠 게 없네. main 포인터를 F2로 밀어 주기만 하면 끝이잖아?" 그래서 새 커밋을 만들지 않고 브랜치 포인터만 앞으로 옮긴다. 이게 fast-forward, 말 그대로 빨리감기다. 비디오테이프를 재생하지 않고 앞으로 감듯이, 이미 존재하는 역사를 따라 포인터만 전진한다.

    diagram

    다이어그램 설명. merge 후의 모습인데 커밋 그래프는 하나도 변하지 않았다. 달라진 건 "F2" 커밋을 main이라는 이름표도 함께 가리키게 됐다는 것뿐이다. 여기서 브랜치의 정체가 드러난다 — 브랜치는 커밋 뭉치가 아니라 특정 커밋 하나를 가리키는 이름표(포인터)다. 이름표를 옮기는 데는 새 커밋이 필요 없다.

    fast-forward가 불가능해지는 순간

    반대로 feature 작업 중에 main에도 커밋 "C3"가 들어왔다면, main은 더 이상 feature의 조상이 아니다. 서로 상대가 모르는 커밋을 갖고 있다.

    diagram

    다이어그램 설명. "C2"에서 갈래가 생겼다. main 포인터를 "F2"로 그냥 밀어 버리면 "C3 — main만 아는 커밋"이 main의 역사에서 떨어져 나간다. 즉 포인터 이동만으로는 두 갈래를 다 담을 수 없다. 이때 git은 fast-forward를 포기하고, 부모를 두 개 가진 merge commit을 만들어 봉합한다. 어제는 매듭 커밋이 생기고 오늘은 안 생긴 미스터리의 답이 이것이다 — 어제는 갈래가 있었고, 오늘은 없었다.

    --no-ff — 빨리감기가 가능해도 일부러 매듭을 남기는 이유

    # fast-forward가 가능한 상황에서도 강제로 merge commit을 만든다
    git merge --no-ff feature
    
    # 반대로, fast-forward가 불가능하면 merge를 거부하게 한다
    git merge --ff-only feature
    

    코드 설명. --no-ff(no fast-forward)는 "빨리감기가 가능해도 하지 말고 매듭 커밋을 만들어라"는 지시다. 왜 굳이? "이 커밋 묶음이 하나의 기능이었다"는 경계를 역사에 남기기 위해서다. fast-forward로 합치면 F1, F2가 main의 다른 커밋들과 구분 없이 한 줄로 섞여, 나중에 "로그인 기능이 어느 커밋부터 어느 커밋까지였지?"를 알 수 없다. 매듭이 있으면 그 매듭 하나가 기능의 봉투 역할을 한다. 실제로 GitHub의 "Merge commit" 버튼은 항상 --no-ff로 동작한다 — PR이라는 묶음 단위를 역사에 남기는 게 목적이기 때문이다. 반대편의 --ff-only는 "매듭 커밋이 생길 상황이면 아예 실패해라"로, 히스토리를 일직선으로만 유지하려는 팀이 안전장치로 쓴다.

    매일 만나는 fast-forward — git pull

    사실 fast-forward를 가장 자주 만나는 곳은 merge 명령이 아니라 git pull이다. pull은 "원격의 새 커밋을 받아오기(fetch) + 내 브랜치에 합치기(merge)"의 축약이라서, 매번 merge의 규칙이 그대로 적용된다.

    diagram

    다이어그램 설명. "git pull 실행" 후 git이 던지는 질문은 하나 — 내 로컬에만 있는 커밋이 있는가. 없으면(내가 커밋 없이 받기만 하는 입장이면) 조용히 빨리감기로 끝난다. 있으면 원격과 내 로컬이 갈래를 이룬 상태라 봉합이 필요한데, 이때 아무 설정이 없으면 최신 git은 "divergent branches를 어떻게 reconcile할지 지정하라"는 힌트를 내며 멈춘다. pull을 했을 뿐인데 갑자기 merge commit이 생기거나 경고를 만나는 순간이 바로 이 갈림길이다. 팀 동료들의 흔한 선택은 git config pull.rebase true — 받아온 원격 커밋 뒤에 내 로컬 커밋을 rebase로 옮겨 심어, pull 때마다 생기는 자잘한 매듭 커밋("Merge branch 'main' of ...")을 없애는 설정이다.

    정리 — fast-forward는 기능이 아니라 판정이다

    fast-forward는 내가 고르는 옵션이라기보다, "갈라진 적이 있는가"에 대한 git의 판정이다. 갈래가 없으면 포인터만 전진하고, 갈래가 있으면 매듭이 생긴다. 이 판정 기준 하나를 알고 나면 "왜 오늘은 merge commit이 안 생기지?", "pull 했는데 왜 merge 커밋이 생겼지?" 같은 미스터리가 전부 같은 답으로 풀린다. 그다음에 남는 것은 취향과 팀 규칙의 문제 — 매듭을 남길 것인가(--no-ff), 일직선을 강제할 것인가(--ff-only, pull.rebase) — 뿐이다.


    참고한 공개 자료:

    • Pro Git Book — Basic Branching and Merging (fast-forward 정의): https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
    • git-merge 공식 문서 (--ff, --no-ff, --ff-only): https://git-scm.com/docs/git-merge
    • git-pull 공식 문서 (divergent branches 처리): https://git-scm.com/docs/git-pull
    • GitHub Docs — About pull request merges: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges

    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.