배포 PR을 Squash Merge했더니 역류 PR이 생긴 이유
1. 배포 PR을 Squash Merge한 이유
프로젝트에서 다음과 같은 브랜치 전략을 쓰고 있었다.
1
feature/* → develop → main
기능 개발은 feature/* 브랜치에서 진행하고, 완료되면 develop으로 PR을 만든다. 배포할 때는 develop → main PR을 생성해서 병합한다.
이전 배포에서 develop → main PR을 Squash Merge로 병합했다. 이유는 단순했다. 배포 PR에는 여러 기능 브랜치에서 들어온 커밋이 누적되어 있으니, 이걸 하나의 깔끔한 커밋으로 정리하는 게 맞다고 생각했다. 실제로 기능 PR에서도 Squash Merge를 써서 커밋을 정리하고 있었기 때문에, 배포 PR에도 같은 방식을 적용하는 게 자연스럽게 느껴졌다.
코드 자체에는 별다른 문제가 없었다. 그런데 이후 GitHub에서 main → develop 방향의 PR이 필요한 것처럼 보이는 상황이 발생했다. 처음에는 누군가 main에 직접 커밋한 건가 의심했지만, 그런 이력은 없었다.
원인을 파악하면서 develop과 main처럼 계속 유지되는 장기 브랜치 사이에서는 Squash Merge가 적합하지 않을 수 있다는 점을 알게 되었다.
2. 코드가 같은데 왜 브랜치가 달라졌을까
먼저 용어를 정리하고 가자.
- 커밋 해시(commit hash): 각 커밋을 식별하는 고유한 SHA 값이다. 파일 내용뿐 아니라 작성자, 시간, 부모 커밋 정보까지 포함해서 해시가 결정된다.
- 커밋 계보(commit lineage): 커밋의 부모-자식 관계로 이어지는 연결 구조다. Git은 이 계보를 따라가며 브랜치의 히스토리를 파악한다.
- 공통 조상(common ancestor): 두 브랜치가 갈라지기 전 마지막으로 공유한 커밋이다. Git은 이 커밋을 기준으로 각 브랜치에서 어떤 변경이 있었는지 계산한다.
여기서 중요한 구분이 하나 있다.
파일의 최종 내용이 같다는 것과 Git 커밋 그래프가 같다는 것은 다른 개념이다.
develop과 main의 파일 내용이 완전히 동일하더라도, 두 브랜치에 기록된 커밋의 해시와 계보가 다르면 Git은 이 둘을 다른 히스토리로 인식한다. 그리고 GitHub PR 비교는 단순히 현재 파일 내용만 대조하는 것이 아니라, 공통 조상을 기준으로 각 브랜치에서 발생한 변경 사항을 계산해서 보여준다.
이 차이를 만들어내는 것이 바로 Squash Merge다.
3. Squash Merge가 만드는 새로운 커밋
Merge Commit, Squash Merge, Rebase Merge의 차이
세 가지 병합 방식이 커밋 그래프에 미치는 영향을 비교해보자.
Merge Commit
1
2
3
B -- C -- D develop
/ \
A -- M main
develop의 커밋 B, C, D를 그대로 유지하면서 병합 커밋 M을 생성한다. M은 두 개의 부모를 갖는다. 하나는 main의 이전 커밋 A, 다른 하나는 develop의 마지막 커밋 D다.
develop의 커밋 히스토리가main에도 그대로 연결된다.- 공통 조상이 명확하게 유지된다.
- 이후 두 브랜치를 비교할 때 Git이 이미 병합된 커밋을 정확히 인식할 수 있다.
Squash Merge
1
2
3
A -- B -- C -- D develop
A -- S main
develop의 B, C, D 변경 내용을 모두 합쳐서 새로운 단일 커밋 S를 만든다. S의 부모는 main의 이전 커밋 A뿐이다. develop의 커밋과는 어떤 부모-자식 관계도 형성되지 않는다.
S와D의 파일 내용은 같을 수 있다.- 하지만 커밋 해시가 다르고, 부모 관계도 다르다.
- Git 입장에서
S는develop과 무관한 새로운 커밋이다.
Rebase Merge
1
2
3
A -- B -- C -- D develop
A -- B' -- C' -- D' main
develop의 각 커밋을 main 위에 하나씩 재적용(replay)한다. 원래 커밋 B, C, D와 내용은 같지만 부모가 달라지므로 해시가 바뀐다.
- 선형 히스토리를 만들 수 있다.
- 하지만 원래 커밋과 해시가 다른 새로운 커밋이 생긴다.
- 여러 개발자가 공유하는 장기 브랜치에서는 히스토리 추적이 복잡해질 수 있다.
핵심: Squash Merge는 계보를 끊는다
develop에 다음과 같은 커밋이 있다고 하자.
1
A -- B -- C -- D develop
여기서 develop → main을 Squash Merge하면 이렇게 된다.
1
2
3
A -- B -- C -- D develop
\
S main
D와 S의 파일 내용은 동일할 수 있다. 하지만 S는 B, C, D와 계보상 아무 관계가 없는 커밋이다. Git에게 S는 develop에서 온 것이 아니라 main에서 독자적으로 만들어진 변경이다.
Merge Commit은 develop의 커밋 계보를 main에 연결하지만, Squash Merge는 같은 코드를 담은 새로운 커밋을 독립적으로 생성한다.
4. Git은 파일이 아니라 커밋 그래프를 비교한다
Git이 두 브랜치의 차이를 계산하는 방식을 이해하면, 왜 코드가 같은데도 차이가 발생하는지 알 수 있다.
Git과 GitHub의 PR 비교는 다음 순서로 동작한다.
- 두 브랜치의 공통 조상(merge base)을 찾는다.
- 공통 조상을 기준으로 각 브랜치에서 어떤 커밋이 추가됐는지 계산한다.
- 추가된 커밋의 변경 내용을 diff로 보여준다.
Merge Commit으로 병합하면 develop과 main의 공통 조상이 갱신된다. main에 들어간 병합 커밋이 develop의 커밋을 부모로 참조하기 때문이다. 이후 두 브랜치를 비교하면 이미 병합된 커밋은 양쪽 모두의 히스토리에 포함되므로 차이로 나타나지 않는다.
Squash Merge는 다르다. main에 생긴 커밋 S는 develop의 커밋과 계보가 끊어져 있다. 공통 조상은 Squash Merge 이전 시점에 머물러 있다. Git이 이 공통 조상을 기준으로 비교하면 main에는 S라는 커밋이 있고, develop에는 B, C, D가 있으며, 이 둘은 서로 다른 커밋으로 취급된다.
즉, Git은 “파일이 같은가?”가 아니라 “공통 조상 이후에 어떤 커밋이 추가됐는가?”를 기준으로 판단한다.
5. 왜 main → develop 역류 PR이 보였을까
역류 PR이란 배포 방향과 반대인 main → develop 방향의 PR을 의미한다. 정상적인 흐름에서는 코드가 develop에서 main으로 흐른다. 반대 방향의 PR이 필요해 보인다는 것은 main에 develop이 모르는 변경이 있다는 뜻이다.
Squash Merge 이후 main에는 develop에 존재하지 않는 커밋 S가 있다. GitHub에서 main → develop을 비교하면 이 커밋이 변경 사항으로 나타날 수 있다. 그래서 마치 main의 변경을 develop에 역류시켜야 할 것처럼 보인다.
다만 이 상황을 정확히 이해하려면 몇 가지를 구분해야 한다.
- 항상 코드 충돌이 발생하는 것은 아니다. 파일 내용이 동일하면 실제 충돌 없이 병합되거나, 변경 파일 목록이 비어 있을 수도 있다.
- 실제 파일 내용이 같다면 변경 파일이 거의 없거나 없을 수도 있다. GitHub PR 화면에서 “Files changed”가 0개로 나타나는 경우도 있다.
- 핵심 문제는 즉각적인 코드 오류가 아니다. 진짜 문제는 장기적으로 브랜치 히스토리 추적이 복잡해지는 것이다.
- 이후 배포 PR에서 혼란이 생긴다. 다음에
develop → mainPR을 만들 때, 이미 배포한 변경 내용이 다시 diff에 표시되거나 커밋 수가 예상보다 많아질 수 있다.
6. 이전에는 왜 문제가 없었던 것처럼 보였을까
같은 Squash Merge 방식으로 이전 배포를 했는데 그때는 역류 PR이 보이지 않았다. 몇 가지 가능성을 생각해볼 수 있다.
- 파일 내용이 동일해서 실제 충돌이 발생하지 않았다. Squash Merge로 계보가 끊어졌더라도 양쪽 브랜치의 파일이 같으면 Git이 자동으로 해결하거나, 아예 변경이 없는 것으로 보일 수 있다.
- 변경량이 작아 히스토리 차이를 체감하지 못했다. 커밋 수가 적고 변경 파일이 소수라면, 비교 결과의 이상을 알아채기 어렵다.
- GitHub PR 화면에서 변경 파일이 없거나 적게 나타났다. 실제로 diff가 비어 있으면 문제를 인식할 계기가 없다.
- 이후 커밋이 기존 변경 내용을 덮어써 문제가 드러나지 않았다. 배포 이후
develop에서 해당 파일을 수정하는 커밋이 들어오면, 이전 Squash Merge로 인한 히스토리 차이가 자연스럽게 가려질 수 있다. - 한두 번의 배포에서는 문제가 작지만, 반복될수록 히스토리가 복잡해진다. Squash Merge를 반복하면
main에는develop이 모르는 커밋이 계속 쌓인다. 배포 횟수가 적을 때는 영향이 미미하지만, 누적되면 PR 비교와 히스토리 추적이 점점 어려워진다.
확실한 원인을 하나로 단정하기는 어렵지만, 위 요인들이 복합적으로 작용해서 문제가 겉으로 드러나지 않았을 가능성이 높다.
7. 기능 PR과 배포 PR의 머지 방식은 달라야 한다
이번 경험을 통해 가장 크게 느낀 점은 브랜치의 수명에 따라 적합한 머지 방식이 다르다는 것이다.
기능 브랜치: Squash Merge 사용 가능
1
feature/* → develop
- 기능 하나를 하나의 의미 있는 커밋으로 정리할 수 있다.
- 작은 작업 커밋, 오타 수정, 리뷰 반영 커밋 등을 깔끔하게 합칠 수 있다.
- 기능 브랜치는 병합 후 삭제되는 단기 브랜치이므로, 계보가 끊어지더라도 히스토리 추적에 큰 영향을 주지 않는다.
develop에는 기능 단위로 정리된 커밋이 쌓이므로 히스토리가 오히려 읽기 쉬워진다.
배포 브랜치: Merge Commit 권장
1
develop → main
develop의 기존 커밋과 계보를main에 그대로 연결한다.- 어떤 기능이 어떤 배포에 포함됐는지 추적하기 쉽다.
- 이후
develop과main을 비교할 때, 이미 배포한 커밋이 명확하게 구분된다. - 불필요한 역류 PR이 발생하지 않는다.
긴급 수정: 역류 병합이 필요한 경우
1
2
hotfix/* → main
main → develop
- 운영 장애나 긴급 수정이
main에서 먼저 반영됐다면, 반드시develop에도 역류 병합해야 한다. - 이 경우의 역류 PR은 정상적이고 필요한 작업이다.
- 배포 PR을 Squash Merge해서 생기는 불필요한 역류와는 구분해야 한다. 전자는 실제 코드 차이를 동기화하는 것이고, 후자는 히스토리 불일치로 생긴 허상이다.
Rebase Merge는 어떨까
Rebase Merge도 선형 히스토리를 만들 수 있어서 매력적으로 보인다. 하지만 develop과 main처럼 여러 개발자가 공유하고 장기간 유지하는 브랜치 사이에서는 주의가 필요하다.
- Rebase는 기존 커밋의 해시를 변경한다. 같은 내용이라도 부모가 달라지면 해시가 바뀐다.
develop에 있던 커밋B와 Rebase 후main에 생긴B'는 내용은 같지만 해시가 다른 별개의 커밋이다.- 이후 두 브랜치를 비교하면 Squash Merge와 비슷한 혼란이 발생할 수 있다.
Rebase Merge가 무조건 나쁜 방식은 아니다. 개인 브랜치나 소규모 작업에서는 유용하다. 다만 이 프로젝트에서는 운영 복잡도와 팀 이해도를 고려했을 때, 배포 PR에 Merge Commit을 사용하는 것이 더 현실적이라는 결론을 내렸다.
feature에서는 Squash Merge로 커밋을 정리하고, develop에서 main으로는 Merge Commit으로 계보를 유지한다. main에서 develop으로의 역류는 운영 긴급 수정 시에만 수행한다.
8. 프로젝트에 적용할 최종 브랜치 규칙
머지 방식
1
2
3
4
feature/* → develop : Squash Merge
develop → main : Merge Commit
hotfix/* → main : 상황에 따라 Squash 또는 Merge
main → develop : main에서 직접 수정된 변경이 있을 때만 Merge Commit
운영 원칙
main과develop에 직접 push하지 않는다.- 모든 변경은 PR을 통해 반영한다.
- 기능 단위 커밋 정리는 feature PR에서 수행한다.
- 배포 PR에서는 커밋을 다시 압축하지 않는다.
- 운영 환경에서 직접 수정된 변경은 develop으로 반드시 역류시킨다.
- GitHub 브랜치 보호 규칙으로 직접 push와 승인 없는 병합을 제한한다.
9. 이번 경험을 통해 배운 점
Squash Merge가 잘못된 방식인 것은 아니다. 기능 브랜치에서는 여전히 유용하고, 커밋 히스토리를 깔끔하게 유지하는 데 효과적이다.
다만 수명이 짧은 기능 브랜치와 장기간 유지되는 배포 브랜치는 목적이 다르다. 기능 브랜치에서는 불필요한 중간 커밋을 정리하는 것이 중요하지만, develop과 main 사이에서는 기존 커밋의 계보를 유지하는 것이 더 중요하다.
처음에는 “커밋이 깔끔하면 좋지 않을까”라는 단순한 판단으로 Squash Merge를 선택했다. 하지만 Git의 병합 방식이 파일 내용뿐 아니라 커밋 그래프와 공통 조상에 어떤 영향을 주는지 이해하고 나니, 브랜치의 역할에 따라 머지 전략을 구분해야 하는 이유가 분명해졌다.
기능 PR에는 Squash Merge를, 배포 PR에는 Merge Commit을 사용하는 전략이 현재 프로젝트에 더 적합하다.
관련 글
- 배포 준비와 다음 기능 개발을 병렬로 진행하기 위한 Git 브랜치 전략 —
develop과 릴리스 브랜치를 분리해서 배포 안정화와 기능 개발을 동시에 진행한 경험을 정리한 글이다. - 로컬 develop 오염으로 PR 커밋이 섞인 문제와 해결 — 브랜치 전략을 따르더라도 로컬에서 실수하면 PR에 의도하지 않은 커밋이 포함될 수 있다는 점을 다룬 글이다.
- PR은 리뷰 요청이 아니라 배포 실패를 막는 첫 번째 검증 지점이다 — PR의 역할을 코드 리뷰를 넘어 자동화된 검증 관문으로 바라보는 관점을 정리한 글이다.
- CI/CD는 YAML 작성이 아니라 안정적인 배포와 복구를 설계하는 과정이었다 — 배포 파이프라인의 본질이 도구 문법이 아니라 배포 전략과 복구 흐름 설계에 있다는 점을 다룬 글이다.
- 협업의 문턱, 깃허브 코드 리뷰와 Pending의 늪 — 브랜치 보호 규칙과 PR 리뷰 프로세스를 처음 도입하면서 겪은 시행착오를 정리한 글이다.
- 구현보다 먼저: 팀 프로젝트를 위한 GitHub 협업 환경 구축기 — Issue, PR Template, Kanban Board 등 GitHub 협업 환경을 구축한 과정을 정리한 글이다.

