<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Version Control on kenji.blog</title><link>http://kenji.blog/ko/tags/version-control/</link><description>Recent content in Version Control on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>ko</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 09:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/ko/tags/version-control/index.xml" rel="self" type="application/rss+xml"/><item><title>【Git 명령】rebase와 merge의 차이와 실무에서의 올바른 사용법</title><link>http://kenji.blog/ko/p/git-rebase-vs-merge-practical-guide/</link><pubDate>Sun, 13 Sep 2026 09:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/git-rebase-vs-merge-practical-guide/</guid><description>&lt;img src="http://kenji.blog/p/git-rebase-vs-merge-practical-guide/img/eyecatch.jpg" alt="Featured image of post 【Git 명령】rebase와 merge의 차이와 실무에서의 올바른 사용법" />&lt;h1 id="1-들어가며왜-merge인가-rebase인가는-영원한-숙제인가">1. 들어가며：왜 &amp;ldquo;merge인가 rebase인가&amp;quot;는 영원한 숙제인가
&lt;/h1>&lt;p>Git은 현대 소프트웨어 개발에 있어서 필수적인 버전 관리 시스템입니다. 여러 개발자가 동시에 코드베이스를 변경할 때, Git의 강력한 브랜치 모델이 위력을 발휘합니다. 하지만, 팀 개발에서 &amp;ldquo;&lt;code>merge&lt;/code>와 &lt;code>rebase&lt;/code> 중 어느 것을 사용해야 하는가&amp;quot;라는 논쟁은 초보자부터 숙련자까지 항상 개발자를 괴롭히는 주제 중 하나입니다.&lt;/p>
&lt;p>이 글에서는 Git의 내부 구조인 DAG(방향 비순환 그래프)와 커밋 해시의 수학적 성질을 통해, &lt;code>git merge&lt;/code>와 &lt;code>git rebase&lt;/code>의 메커니즘 차이를 깊이 파헤칩니다. 그리고 실무에서 이것들을 어떻게 구분해서 사용해야 하는지, 구체적인 워크플로우와 함께 철저하게 해설합니다. 단순한 명령어 소개에 그치지 않고, 이면에서 Git이 어떤 계산을 하고 있는지까지 이해함으로써, 충돌(Conflict)에 대한 두려움을 없애고 깔끔하고 추적 가능한 이력을 구축할 수 있게 됩니다.&lt;/p>
&lt;hr>
&lt;h1 id="2-git의-내부-구조커밋-해시와-객체-모델">2. Git의 내부 구조：커밋 해시와 객체 모델
&lt;/h1>&lt;p>Git이 이력을 어떻게 통합하는지 이해하기 위해서는 먼저 Git이 데이터를 어떻게 저장하고 있는지 알아야 합니다. Git은 단순한 파일 변경의 차이(패치)를 저장하는 것이 아니라, 특정 시점의 파일 시스템 전체의 스냅샷을 저장하고 있습니다.&lt;/p>
&lt;h2 id="21-커밋-해시의-암호학적-성질">2.1 커밋 해시의 암호학적 성질
&lt;/h2>&lt;p>Git의 각 커밋은 그 내용을 바탕으로 계산된 SHA-1(Secure Hash Algorithm 1) 해시 함수에 의한 40자리 16진수로 고유하게 식별됩니다. 커밋 객체는 다음 요소들로 구성됩니다:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Tree 객체에 대한 포인터&lt;/strong>: 그 시점의 디렉토리 구조와 파일(Blob)의 스냅샷&lt;/li>
&lt;li>&lt;strong>부모 커밋에 대한 포인터&lt;/strong>: 1개 이상의 부모 커밋의 해시값 (첫 커밋은 부모를 가지지 않으며, 병합 커밋은 2개 이상의 부모를 가집니다)&lt;/li>
&lt;li>&lt;strong>작성자 정보(Author)&lt;/strong>: 코드를 작성한 사람과 일시&lt;/li>
&lt;li>&lt;strong>커미터 정보(Committer)&lt;/strong>: 커밋을 생성하고 적용한 사람과 일시&lt;/li>
&lt;li>&lt;strong>커밋 메시지&lt;/strong>: 변경의 의도를 설명하는 텍스트&lt;/li>
&lt;/ol>
&lt;p>수학적으로 표현하면, 커밋 객체 $C$ 에 대한 해시값 $H(C)$ 는 다음과 같이 정의됩니다：&lt;/p>
$$
H(C) = \text{SHA-1}( \text{tree} \parallel \text{parent} \parallel \text{author} \parallel \text{committer} \parallel \text{message} )
$$&lt;p>여기서 $\parallel$ 은 데이터의 결합을 나타냅니다. 해시 함수의 특성에 따라, 커밋 메시지를 1글자만 바꾸거나 부모 커밋이 다르기만 해도 전혀 다른 해시값이 생성됩니다. 즉, &lt;strong>커밋은 불변(Immutable)&lt;/strong> 입니다. 후술할 &lt;code>rebase&lt;/code>가 &amp;ldquo;이력을 조작한다&amp;quot;고 표현되는 것은 실제로는 &amp;ldquo;내용은 비슷하지만 다른 해시값을 가지는 새로운 커밋을 생성하고 있기&amp;rdquo; 때문입니다.&lt;/p>
&lt;p>해시 공간의 크기는 $2^{160}$ 이며, 충돌(다른 커밋이 같은 해시값을 가지는 것)이 발생할 확률 $P$ 는 생일 문제(Birthday Paradox)의 이론을 사용하면 다음과 같이 근사할 수 있습니다 ($n$ 은 커밋 수)：&lt;/p>
$$
P(\text{collision}) \approx 1 - \exp\left(-\frac{n^2}{2 \times 2^{160}}\right)
$$&lt;p>이 확률은 극히 낮아 실용상 Git의 커밋 해시가 충돌하는 일은 거의 있을 수 없습니다.&lt;/p>
&lt;hr>
&lt;h1 id="3-그래프-이론과-daggit-이력의-수학적-모델">3. 그래프 이론과 DAG：Git 이력의 수학적 모델
&lt;/h1>&lt;p>Git의 커밋 이력은 그래프 이론의 &amp;ldquo;방향 비순환 그래프(Directed Acyclic Graph, DAG)&amp;ldquo;로 모델링됩니다.&lt;/p>
&lt;h2 id="31-dag방향-비순환-그래프란">3.1 DAG(방향 비순환 그래프)란
&lt;/h2>&lt;p>그래프 $G = (V, E)$ 에서, $V$ 는 커밋의 집합(정점), $E$ 는 커밋 간의 부모-자식 관계를 나타내는 방향 간선(Directed Edge)의 집합입니다. Git에서는 간선의 방향이 &amp;ldquo;자식 커밋에서 부모 커밋&amp;quot;으로 향합니다. 새로운 커밋은 과거의 커밋에 대한 포인터를 가지고 있기 때문입니다.&lt;/p>
&lt;pre class="mermaid">
graph BT
A[&amp;#34;Commit A (초기)&amp;#34;]
B[&amp;#34;Commit B&amp;#34;]
C[&amp;#34;Commit C (Main)&amp;#34;]
D[&amp;#34;Commit D (Feature)&amp;#34;]
E[&amp;#34;Commit E (Merge)&amp;#34;]
B --&amp;gt; A
C --&amp;gt; B
D --&amp;gt; B
E --&amp;gt; C
E --&amp;gt; D
&lt;/pre>
&lt;p>DAG의 가장 큰 특징은 &amp;ldquo;사이클(순환)이 존재하지 않는다&amp;quot;는 것입니다. 이로 인해 커밋 이력을 거슬러 올라가는 알고리즘은 무한 루프에 빠지지 않고 확실하게 종단(초기 커밋)에 도달할 수 있습니다.&lt;/p>
&lt;h2 id="32-위상-정렬topological-sort과-이력의-순서">3.2 위상 정렬(Topological Sort)과 이력의 순서
&lt;/h2>&lt;p>Git의 &lt;code>git log&lt;/code> 명령어 등으로 이력을 표시할 때, DAG는 위상 정렬(Topological Sort) 알고리즘에 의해 1차원의 리스트로 순서가 매겨집니다. DAG에서의 임의의 방향 간선 $u \to v$ ($u$ 는 $v$ 의 자식)에 대해, 리스트 내에서 $u$ 가 $v$ 보다 앞에 오도록 정렬합니다.&lt;/p>
&lt;hr>
&lt;h1 id="4-git-merge의-메커니즘과-종류">4. git merge의 메커니즘과 종류
&lt;/h1>&lt;p>브랜치의 변경을 통합하는 가장 기본적인 명령어가 &lt;code>git merge&lt;/code>입니다. 하지만 현재 상태에 따라 Git은 다른 병합 전략을 자동으로 선택합니다.&lt;/p>
&lt;h2 id="41-fast-forward-병합---ff">4.1 Fast-Forward 병합 (&amp;ndash;ff)
&lt;/h2>&lt;p>통합 대상(예: &lt;code>main&lt;/code>)의 브랜치가 통합할(예: &lt;code>feature&lt;/code>) 브랜치의 직접적인 조상일 경우, Git은 &amp;ldquo;Fast-Forward(빨리 감기)&amp;rdquo; 병합을 실행합니다. 이는 새로운 커밋을 생성하지 않고 단순히 브랜치의 포인터를 앞으로 이동시키는 작업입니다.&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
merge feature
&lt;/pre>
&lt;p>Fast-Forward 병합은 이력을 일직선으로 유지하지만, &amp;ldquo;어떤 커밋들이 하나의 기능 개발(feature)로 묶여 있었는지&amp;quot;에 대한 컨텍스트가 손실되는 단점이 있습니다.&lt;/p>
&lt;h2 id="42-non-fast-forward-병합---no-ff">4.2 Non-Fast-Forward 병합 (&amp;ndash;no-ff)
&lt;/h2>&lt;p>&lt;code>git merge --no-ff&lt;/code>를 명시적으로 지정하면, Fast-Forward가 가능한 상황이더라도 반드시 새로운 &amp;ldquo;병합 커밋&amp;quot;을 생성합니다. 병합 커밋은 2개의 부모를 가지는 특별한 커밋입니다.&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
commit id: &amp;#34;Main 작업 1&amp;#34;
merge feature type: NORMAL
&lt;/pre>
&lt;p>이 방법의 이점은 기능 브랜치의 존재와 역사가 DAG 상에 명확하게 남는다는 것입니다. 문제가 발생했을 때 &lt;code>git revert -m 1 &amp;lt;병합 커밋의 해시&amp;gt;&lt;/code>를 실행하여 기능 전체를 한 번에 안전하게 취소(리버트)할 수 있습니다.&lt;/p>
&lt;h2 id="43-3방향-병합3-way-merge-알고리즘">4.3 3방향 병합(3-Way Merge) 알고리즘
&lt;/h2>&lt;p>통합 대상과 통합할 브랜치가 각각 독자적인 커밋을 가지고 있는 경우, Git은 3방향 병합을 실행합니다. 이 때 Git은 DAG를 탐색하여 두 브랜치의 &amp;ldquo;공통 조상(Lowest Common Ancestor, LCA)&amp;ldquo;을 찾아냅니다.&lt;/p>
&lt;p>LCA를 찾기 위한 알고리즘의 시간 복잡도 $T_{\text{LCA}}$ 는 정점 수 $|V|$ 와 간선 수 $|E|$ 에 대해 선형 시간에 실행 가능합니다：&lt;/p>
$$
T_{\text{LCA}} = \mathcal{O}(|V| + |E|)
$$&lt;p>Git은 &amp;ldquo;LCA의 상태&amp;rdquo;, &amp;ldquo;현재 브랜치의 상태&amp;rdquo;, &amp;ldquo;상대 브랜치의 상태&amp;rdquo; 3가지를 비교하여 변경 사항이 충돌하지 않으면 자동으로 병합 커밋을 생성합니다.&lt;/p>
&lt;hr>
&lt;h1 id="5-git-rebase의-메커니즘과-이력의-재구성">5. git rebase의 메커니즘과 이력의 재구성
&lt;/h1>&lt;p>&lt;code>git merge&lt;/code>가 이력을 &amp;ldquo;통합&amp;quot;하는 반면, &lt;code>git rebase&lt;/code>는 이력을 &amp;ldquo;재구성(다시 붙이기)&amp;ldquo;합니다.&lt;/p>
&lt;h2 id="51-rebase의-이면에-있는-동작">5.1 Rebase의 이면에 있는 동작
&lt;/h2>&lt;p>&lt;code>feature&lt;/code> 브랜치를 &lt;code>main&lt;/code> 브랜치로 리베이스(&lt;code>git rebase main&lt;/code>)할 때의 내부 동작은 다음과 같습니다：&lt;/p>
&lt;ol>
&lt;li>&lt;code>feature&lt;/code> 브랜치와 &lt;code>main&lt;/code> 브랜치의 공통 조상(LCA)을 찾는다.&lt;/li>
&lt;li>LCA부터 &lt;code>feature&lt;/code> 브랜치 끝까지의 커밋 차이를 임시 영역에 저장한다.&lt;/li>
&lt;li>&lt;code>feature&lt;/code> 브랜치의 포인터를 &lt;code>main&lt;/code> 브랜치의 끝으로 이동시킨다.&lt;/li>
&lt;li>저장해둔 차이를 새로운 베이스(&lt;code>main&lt;/code>의 끝) 위에 하나씩 순차적으로 적용(Cherry-Pick)하여 새로운 커밋을 생성한다.&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;Commit A&amp;#34;] --&amp;gt; B[&amp;#34;Commit B&amp;#34;]
B --&amp;gt; C[&amp;#34;Commit C (Main)&amp;#34;]
B --&amp;gt; D[&amp;#34;Commit D (이전 Feature)&amp;#34;]
D -.-&amp;gt; E[&amp;#34;Commit D&amp;#39; (새로운 Feature)&amp;#34;]
C --&amp;gt; E
style D stroke-dasharray: 5 5, fill: #f9f9f9, color: #999
&lt;/pre>
&lt;p>여기서 중요한 것은 리베이스로 인해 생성된 커밋 $D'$ 는 원래의 커밋 $D$ 와 &lt;strong>부모 커밋이 다르기 때문에 완전히 다른 해시값을 가진다&lt;/strong>는 것입니다 (앞서 설명한 해시 함수의 정의 $H(C)$ 참조).&lt;/p>
&lt;h2 id="52-인터랙티브-리베이스interactive-rebase">5.2 인터랙티브 리베이스(Interactive Rebase)
&lt;/h2>&lt;p>&lt;code>git rebase -i&lt;/code> (또는 &lt;code>--interactive&lt;/code>)를 사용하면 커밋 이력을 마음대로 조작할 수 있습니다. 이는 로컬 이력을 정리하기 위한 가장 강력한 도구입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>pick&lt;/code> : 커밋을 그대로 적용한다.&lt;/li>
&lt;li>&lt;code>reword&lt;/code> : 커밋 메시지만 수정한다.&lt;/li>
&lt;li>&lt;code>edit&lt;/code> : 커밋 내용을 수정하기 위해 일시 정지한다.&lt;/li>
&lt;li>&lt;code>squash&lt;/code> : 이 커밋을 이전 커밋에 합치고, 메시지도 결합한다.&lt;/li>
&lt;li>&lt;code>fixup&lt;/code> : &lt;code>squash&lt;/code>와 같지만, 이 커밋의 메시지는 파기한다.&lt;/li>
&lt;li>&lt;code>drop&lt;/code> : 커밋을 완전히 삭제한다.&lt;/li>
&lt;/ul>
&lt;p>수학적으로 보면, 어떤 브랜치에 $N$ 개의 커밋이 있을 경우 리베이스의 순서 변경으로 생성 가능한 선형 이력의 변형(순열) $P$ 는 다음과 같습니다.&lt;/p>
$$
P = N!
$$&lt;p>Git은 개발자에게 $N!$ 가지의 자유를 주어 이력을 논리적이고 아름다운 상태로 유지할 수 있게 해줍니다.&lt;/p>
&lt;hr>
&lt;h1 id="6-리베이스의-황금률the-golden-rule-of-rebase">6. 리베이스의 황금률(The Golden Rule of Rebase)
&lt;/h1>&lt;p>&lt;code>rebase&lt;/code>는 매우 강력하지만 하나의 절대적인 규칙이 존재합니다.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>&amp;ldquo;공개된 퍼블릭한 이력에 대해서는 절대 리베이스를 해서는 안 된다&amp;rdquo;&lt;/strong>
&lt;em>(Never rebase public history)&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;h2 id="61-왜-퍼블릭-이력을-리베이스하면-안-되는가">6.1 왜 퍼블릭 이력을 리베이스하면 안 되는가?
&lt;/h2>&lt;p>Git은 분산형입니다. 당신이 &lt;code>origin/main&lt;/code>에 푸시한 커밋은 다른 개발자의 로컬 저장소에도 클론(복제)되어 있습니다. 만약 당신이 이미 푸시한 커밋을 리베이스하여 이력을 조작하고 &lt;code>git push --force&lt;/code>로 강제 덮어쓰기를 한다면 무슨 일이 일어날까요?&lt;/p>
&lt;p>다른 개발자의 로컬에 있는 DAG와 리모트의 DAG가 근본적으로 분기되어 버립니다. 다른 개발자가 &lt;code>git pull&lt;/code>을 실행하면 Git은 다른 역사를 가진 커밋 뭉치를 억지로 병합하려고 시도하고, 대량의 충돌이나 중복 커밋(동일한 변경 내용이지만 해시가 다른 커밋)이 발생하여 저장소가 패닉 상태에 빠집니다.&lt;/p>
&lt;p>리베이스는 **&amp;ldquo;아직 아무와도 공유하지 않은 로컬 브랜치&amp;rdquo;**에 대해서만 수행하는 것이 철칙입니다.&lt;/p>
&lt;hr>
&lt;h1 id="7-충돌conflict-해결과-git-rebase---continue">7. 충돌(Conflict) 해결과 git rebase &amp;ndash;continue
&lt;/h1>&lt;p>여러 사람이 같은 파일의 같은 부분을 변경한 경우 충돌이 발생합니다. &lt;code>merge&lt;/code>와 &lt;code>rebase&lt;/code>에서는 충돌을 해결하는 과정이 다릅니다.&lt;/p>
&lt;h2 id="71-merge에서의-충돌-해결">7.1 Merge에서의 충돌 해결
&lt;/h2>&lt;p>&lt;code>git merge&lt;/code>의 경우 충돌 해결은 &lt;strong>한 번만&lt;/strong> 발생합니다. 최종적인 병합 커밋을 생성하기 직전에 모든 충돌 지점을 한 번에 수정합니다.&lt;/p>
&lt;h2 id="72-rebase에서의-충돌-해결">7.2 Rebase에서의 충돌 해결
&lt;/h2>&lt;p>&lt;code>git rebase&lt;/code>의 경우 커밋을 하나씩 다시 적용해 나가는 특성상 &lt;strong>각 커밋마다 충돌이 발생할 가능성&lt;/strong>이 있습니다.&lt;/p>
&lt;p>리베이스 중에 충돌이 발생하면 Git은 처리를 일시 정지합니다. 해결 흐름은 다음과 같습니다：&lt;/p>
&lt;ol>
&lt;li>에디터나 IDE(VS Code 등)를 열어 충돌 마커(&lt;code>&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code> 나 &lt;code>======&lt;/code>, &lt;code>&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code>)를 수동으로 수정한다.&lt;/li>
&lt;li>수정한 파일을 인덱스에 추가한다：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add &amp;lt;수정한 파일&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>커밋은 생성하지 않고 리베이스 처리를 재개한다：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>만약 리베이스 자체를 취소하고 원래 상태로 되돌리고 싶다면 다음 명령어를 실행합니다：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --abort
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>(※ 충돌 해결이 필요 없고 해당 커밋을 통째로 건너뛰고 싶을 때는 &lt;code>git rebase --skip&lt;/code>을 사용합니다)&lt;/p>
&lt;hr>
&lt;h1 id="8-실무에서의-올바른-사용법워크플로우-실천">8. 실무에서의 올바른 사용법(워크플로우 실천)
&lt;/h1>&lt;p>그렇다면 실제 개발 현장에서는 &lt;code>merge&lt;/code>와 &lt;code>rebase&lt;/code>를 어떻게 구분해서 사용해야 할까요? 여기서는 가장 표준적이고 안전한 접근 방식을 소개합니다.&lt;/p>
&lt;h2 id="81-시나리오-1-로컬-작업-이력-정리rebase-활용">8.1 【시나리오 1】 로컬 작업 이력 정리(Rebase 활용)
&lt;/h2>&lt;p>기능 브랜치에서 개발 중, 자잘한 커밋(&amp;ldquo;오타 수정&amp;rdquo;, &amp;ldquo;임시 저장&amp;rdquo; 등)이 많이 쌓였다고 가정해 봅시다. 풀 리퀘스트(PR)를 올리기 전에, 이것들을 의미 있는 단위로 정리하기 위해 인터랙티브 리베이스를 사용합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># feature 브랜치에 있는 상태에서 실행&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase -i HEAD~5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (에디터가 열리면 squash나 fixup을 사용하여 이력을 정리한다)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이를 통해 리뷰어에게 의도가 잘 전달되는 아름다운 커밋 이력을 만들 수 있습니다.&lt;/p>
&lt;h2 id="82-시나리오-2-최신-main-브랜치-따라가기rebase-활용">8.2 【시나리오 2】 최신 main 브랜치 따라가기(Rebase 활용)
&lt;/h2>&lt;p>개발이 길어지고 &lt;code>main&lt;/code> 브랜치에 다른 사람들의 변경 사항이 계속 병합되는 경우, 자신의 &lt;code>feature&lt;/code> 브랜치가 구버전이 되어 버립니다. 이 경우 &lt;code>feature&lt;/code> 브랜치를 최신 &lt;code>main&lt;/code>으로 리베이스하여 변경 사항을 쫓아갑니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># main의 최신 정보를 가져옴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># feature 브랜치를 최신 main 위에 다시 이어 붙임&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase origin/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이로 인해 이력이 일직선이 되어 나중에 병합할 때의 충돌을 방지할 수 있습니다. 또한, 불필요한 병합 커밋(&amp;ldquo;Merge branch &amp;lsquo;main&amp;rsquo; into feature&amp;rdquo;)이 생성되는 것을 막을 수 있습니다.&lt;/p>
&lt;h2 id="83-시나리오-3-완성된-기능-통합merge-활용">8.3 【시나리오 3】 완성된 기능 통합(Merge 활용)
&lt;/h2>&lt;p>&lt;code>feature&lt;/code> 브랜치에서의 개발이 완료되고 마침내 &lt;code>main&lt;/code> 브랜치에 통합하는 단계입니다. 여기서는 **&lt;code>git merge --no-ff&lt;/code>**를 사용합니다 (GitHub 등의 Pull Request에서 &amp;ldquo;Create a merge commit&amp;quot;을 선택하는 것과 동일합니다).&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git merge --no-ff feature -m &lt;span class="s2">&amp;#34;Merge feature: 사용자 로그인 기능 구현&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이로 인해 &lt;code>main&lt;/code> 브랜치의 DAG 상에 &amp;ldquo;여기서 하나의 기능이 병합되었다&amp;quot;는 역사의 결절점(병합 커밋)이 남습니다. 나중에 이력을 볼 때 기능 단위로 코드를 추적하기 쉬워집니다.&lt;/p>
&lt;hr>
&lt;h1 id="9-결론요약">9. 결론(요약)
&lt;/h1>&lt;p>Git을 조작할 때 &amp;ldquo;모든 것을 Merge로 해결한다&amp;quot;거나 &amp;ldquo;모든 것을 Rebase로 일직선으로 만든다&amp;quot;와 같은 극단적인 접근 방식은 각각 장단점이 있습니다.&lt;/p>
&lt;p>실무에서의 모범 사례는 **&amp;ldquo;로컬의 프라이빗한 이력은 rebase로 아름답게 정리하고, 퍼블릭한 통합 이력은 merge &amp;ndash;no-ff로 문맥을 남긴다&amp;rdquo;**는 하이브리드 접근 방식입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>로컬(개인의 작업 공간)&lt;/strong>: &lt;code>rebase&lt;/code>를 사용하여 불필요한 커밋을 배제하고, 최신 메인 라인을 따라가며 일직선의 이력을 유지한다.&lt;/li>
&lt;li>&lt;strong>글로벌(공유 작업 공간)&lt;/strong>: &lt;code>merge --no-ff&lt;/code>를 사용하여 기능 브랜치의 존재를 병합 커밋으로 DAG에 기록하고, 리버트나 트래킹을 용이하게 한다.&lt;/li>
&lt;/ul>
&lt;p>DAG의 구조나 해시 함수의 원리 등 수학적, 아키텍처적 배경을 이해함으로써, Git 명령어는 단순한 암기에서 &amp;ldquo;의도를 가진 이력 설계&amp;quot;로 승화됩니다. 리베이스의 황금률을 지키면서 상황에 맞는 최적의 명령어를 선택하여, 팀 전체가 읽기 쉽고 유지 보수하기 편한 깔끔한 커밋 이력을 구축해 나갑시다.&lt;/p></description></item><item><title>Git 초보자가 빠지기 쉬운 실수와 해결 명령어 모음 (충돌 해결 등)</title><link>http://kenji.blog/ko/p/git-beginners-mistakes-and-solutions/</link><pubDate>Sat, 12 Sep 2026 17:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/git-beginners-mistakes-and-solutions/</guid><description>&lt;img src="http://kenji.blog/p/git-beginners-mistakes-and-solutions/img/eyecatch.jpg" alt="Featured image of post Git 초보자가 빠지기 쉬운 실수와 해결 명령어 모음 (충돌 해결 등)" />&lt;h1 id="git-초보자가-빠지기-쉬운-실수와-해결-명령어-모음-충돌-해결-등">Git 초보자가 빠지기 쉬운 실수와 해결 명령어 모음 (충돌 해결 등)
&lt;/h1>&lt;h2 id="1-시작하며-왜-git에서-실수를-하게-될까">1. 시작하며: 왜 Git에서 실수를 하게 될까?
&lt;/h2>&lt;p>소프트웨어 개발에서 Git은 공기나 물처럼 없어서는 안 될 존재가 되었습니다. 하지만 많은 초보자(그리고 때로는 숙련자조차)에게 Git은 &amp;ldquo;무서운 마법의 블랙박스&amp;quot;처럼 느껴질 때가 있습니다. 커밋이 사라지거나, 의도하지 않은 브랜치에 대량의 변경 사항을 밀어넣어 버리거나, 본 적도 없는 충돌 에러 메시지가 화면을 가득 채우거나&amp;hellip; 이런 &amp;ldquo;Git의 덫&amp;quot;에 빠지면 작업 진행이 완전히 멈춰버리고, 최악의 경우 소스 코드를 망가뜨리는 것은 아닐까 하는 공포에 휩싸입니다.&lt;/p>
&lt;p>왜 Git은 이렇게나 어렵고 실수를 유발하기 쉬울까요? 그 가장 큰 이유는 &amp;ldquo;Git 내부에서 무슨 일이 일어나고 있는지 이해하지 못한 채, 표면적인 명령어만 암기해서 사용하고 있기 때문&amp;quot;입니다. Git은 분산 버전 관리 시스템(DVCS)으로서 견고한 설계 사상을 바탕으로 하고 있지만, 그 인터페이스(CLI)가 반드시 직관적인 것은 아닙니다.&lt;/p>
&lt;p>이 글에서는 Git 초보자가 실무에서 자주 겪는 &amp;ldquo;실수&amp;quot;를 여러 케이스로 분류하고, 각각의 구체적인 해결책이 되는 명령어를 제시합니다. 하지만 단순한 명령어의 나열(치트 시트)로 만들지는 않겠습니다. &amp;ldquo;왜 그 실수가 일어나는가&amp;rdquo;, &amp;ldquo;그 명령어를 실행하면 Git 내부에서 데이터가 어떻게 움직이는가&amp;quot;를 &lt;code>.git&lt;/code> 디렉토리의 구조나 백그라운드에서 동작하는 Diff 알고리즘의 수학적 배경, Mermaid를 통한 도해를 곁들여 10,000자가 넘는 분량으로 철저하게 파헤쳐 봅니다.&lt;/p>
&lt;p>이 글을 끝까지 읽었을 때, 당신은 &amp;ldquo;Git이 무섭다&amp;quot;는 감정에서 해방되어 오히려 &amp;ldquo;Git만큼 믿을 수 있는 파트너는 없다&amp;quot;고 확신할 수 있게 될 것입니다. 그러면 Git의 심연 속으로 뛰어들어 보겠습니다.&lt;/p>
&lt;hr>
&lt;h2 id="2-git의-심연-git-디렉토리의-내부-구조-이해하기">2. Git의 심연: &lt;code>.git&lt;/code> 디렉토리의 내부 구조 이해하기
&lt;/h2>&lt;p>많은 트러블슈팅을 쉽게 하기 위한 첫걸음은 Git이 데이터를 어떻게 저장하고 있는지 아는 것입니다. 프로젝트의 루트 디렉토리에 존재하는 숨김 폴더 &lt;code>.git&lt;/code>, 이것이야말로 Git의 심장부입니다. Git은 단순한 파일의 차이(패치)를 순서대로 기록해 나가는 시스템이 아니라, &lt;strong>스냅샷의 스트림&lt;/strong>으로서 데이터를 관리하고 있습니다.&lt;/p>
&lt;h3 id="21-객체-모델-blob-tree-commit">2.1 객체 모델: Blob, Tree, Commit
&lt;/h3>&lt;p>Git은 주로 3가지 객체를 사용하여 리포지토리의 상태를 표현합니다. 이 객체들은 &lt;code>.git/objects&lt;/code>에 저장됩니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Blob (Binary Large Object)&lt;/strong>
파일의 내용 자체를 저장하는 객체입니다. 파일 이름이나 권한 정보는 여기에 포함되지 않습니다. 순수한 바이트 열이 zlib으로 압축되며, SHA-1 해시값(40자의 16진수)에 의해 식별됩니다.&lt;/li>
&lt;li>&lt;strong>Tree&lt;/strong>
디렉토리의 구조를 나타내는 객체입니다. Tree 객체는 다른 Tree 객체(하위 디렉토리)나 Blob 객체(파일)에 대한 포인터(SHA-1 해시값), 그리고 그 파일들의 이름, 접근 권한을 포함합니다. UNIX의 디렉토리와 같은 역할을 합니다.&lt;/li>
&lt;li>&lt;strong>Commit&lt;/strong>
어느 시점에서의 리포지토리 전체의 최상위 Tree 객체에 대한 포인터와, 메타데이터(작성자, 커밋 일시, 커밋 메시지), 그리고 직전 커밋(부모 커밋)에 대한 포인터를 유지합니다.&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
graph TD
Commit1[&amp;#34;커밋 (해시: 9f8a)&amp;#34;] --&amp;gt; Tree1[&amp;#34;트리 (해시: 4b82)&amp;#34;]
Tree1 --&amp;gt; Blob1[&amp;#34;블롭 (해시: 8d7e) : index.js&amp;#34;]
Tree1 --&amp;gt; Tree2[&amp;#34;트리 (해시: 3a2c) : src/&amp;#34;]
Tree2 --&amp;gt; Blob2[&amp;#34;블롭 (해시: 5f1b) : app.js&amp;#34;]
&lt;/pre>
&lt;h3 id="22-head와-참조refs의-정체">2.2 HEAD와 참조(Refs)의 정체
&lt;/h3>&lt;p>Git에서 작업하다 보면 자주 눈에 띄는 &lt;code>HEAD&lt;/code>라는 단어. 이것은 현재 체크아웃하고 있는 브랜치(또는 커밋)를 가리키는 **심볼릭 참조(Symbolic Reference)**입니다.
&lt;code>.git/HEAD&lt;/code>라는 파일을 텍스트 에디터로 열어보면 다음과 같은 문자열이 적혀 있습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ref: refs/heads/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이것은 &amp;ldquo;현재 상태는 &lt;code>main&lt;/code> 브랜치의 끝에 있다&amp;quot;는 것을 의미합니다. 그리고 &lt;code>.git/refs/heads/main&lt;/code>을 열면 거기에 40자의 SHA-1 해시가 적혀 있으며, 이것이 최신 Commit 객체를 가리키고 있는 것입니다.
Git의 브랜치란, 단순히 특정 커밋을 가리키는 가벼운 포인터(파일)에 불과합니다. 이 사실을 알고 있는 것만으로 &amp;ldquo;브랜치를 삭제하면 파일이 전부 사라지는 거 아냐?&amp;rdquo; 하는 공포가 사라집니다.&lt;/p>
&lt;hr>
&lt;h2 id="3-수학으로-읽어내는-git-diff-알고리즘과-해시-함수">3. 수학으로 읽어내는 Git: Diff 알고리즘과 해시 함수
&lt;/h2>&lt;p>Git이 충돌을 감지하거나 파일의 차이를 표시할 때, 내부에서는 고도의 알고리즘이 동작하고 있습니다.&lt;/p>
&lt;h3 id="31-myers의-diff-알고리즘">3.1 Myers의 Diff 알고리즘
&lt;/h3>&lt;p>Git의 기본 차이 감지 알고리즘은 Eugene W. Myers가 고안한 알고리즘입니다. 2개의 텍스트 파일 $A$와 $B$가 있을 때, $A$를 $B$로 변환하기 위한 &amp;ldquo;최소의 편집 절차(삽입과 삭제)&amp;ldquo;를 찾는 문제는 그래프 이론에서의 최단 경로 문제로 모델화할 수 있습니다.&lt;/p>
&lt;p>문자열의 길이를 각각 $N, M$이라 하고, 합계를 $V = N + M$이라고 합니다. Myers의 알고리즘에서는 편집 거리(Edit Distance) $D$를 탐색합니다. 이 알고리즘의 시간 복잡도는 이하의 식으로 표현됩니다.&lt;/p>
$$ \mathcal{O}(V \cdot D) $$&lt;p>여기서 파일 간의 차이가 작을(즉 $D$가 작을) 경우, 알고리즘은 매우 고속 $\mathcal{O}(V)$으로 동작합니다. 하지만 파일이 완전히 다를 경우 $D \approx V$가 되어, 최악 계산량은 $\mathcal{O}(V^2)$가 됩니다.&lt;/p>
&lt;h3 id="32-patience-diff와-histogram-diff">3.2 Patience Diff와 Histogram Diff
&lt;/h3>&lt;p>Myers의 알고리즘은 우수하지만, 함수나 클래스의 순서를 크게 바꾼 경우 등에 인간에게 직관적이지 않은 (의미가 통하지 않는) 차이를 생성할 때가 있습니다. 이를 해결하기 위해 Git은 &lt;code>Patience Diff&lt;/code>와 &lt;code>Histogram Diff&lt;/code>를 구현하고 있습니다.&lt;/p>
&lt;p>Patience Diff는 &amp;ldquo;양쪽 파일에 1번만 등장하는 유니크한 줄&amp;quot;에 주목하여, 그것들의 최장 공통 부분 수열(Longest Common Subsequence: LCS)을 찾습니다. 유니크한 요소의 수를 $U$라고 하면, LCS 계산은 이하의 계산량으로 풀 수 있습니다.&lt;/p>
$$ \mathcal{O}(U \log U) $$&lt;p>충돌 해결이 어렵다고 느껴질 때는 &lt;code>git diff --histogram&lt;/code>이나 병합 전략에 이 알고리즘을 지정하는 것(&lt;code>git merge -s recursive -X histogram&lt;/code>)이 하나의 방법입니다.&lt;/p>
&lt;h3 id="33-sha-1과-충돌-확률">3.3 SHA-1과 충돌 확률
&lt;/h3>&lt;p>Git은 모든 객체를 SHA-1 해시값으로 관리합니다. 해시 공간의 크기는 $2^{160}$입니다. 해시의 충돌(다른 콘텐츠가 같은 해시값을 가지는 것)이 일어날 확률에 대해 생일 역설(Birthday Paradox)을 사용하여 근사하면, 충돌 확률 $p$가 50%가 되기 위해 필요한 객체 수 $k$는 다음과 같습니다.&lt;/p>
$$ k \approx \sqrt{2 \ln(2)} \cdot 2^{80} \approx 1.2 \times 2^{80} $$&lt;p>이것은 천문학적인 숫자이며, 일반적인 소프트웨어 개발에 있어서 의도치 않게 충돌이 발생할 확률은 실질적으로 제로입니다. 그 때문에 Git은 해시값을 &amp;ldquo;절대적인 고유 ID&amp;quot;로서 신뢰하며 동작하고 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="4-케이스-스터디-1-잘못된-브랜치에-커밋해-버렸다">4. 케이스 스터디 1: 잘못된 브랜치에 커밋해 버렸다!
&lt;/h2>&lt;p>&lt;strong>【상황】&lt;/strong>
&lt;code>main&lt;/code> 브랜치에서 작업하고 있다는 것을 깨닫지 못하고 새로운 기능의 코드를 팍팍 작성해 버렸고, 심지어 &lt;code>git commit&lt;/code>까지 해버렸다. 원래는 &lt;code>feature/login&lt;/code>이라는 브랜치를 만들어서 거기서 작업해야 했는데!&lt;/p>
&lt;h3 id="해결-방법-git-reset과-브랜치-생성">해결 방법: &lt;code>git reset&lt;/code>과 브랜치 생성
&lt;/h3>&lt;p>Git에서 커밋은 독립된 객체이며, 브랜치는 그저 포인터입니다. 따라서 &amp;ldquo;새로운 브랜치를 만든 다음, 현재 브랜치의 포인터를 되감는&amp;rdquo; 조작으로 순식간에 해결할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 현재의 커밋(실수로 만든 커밋)을 가리키는 새로운 브랜치를 생성한다&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git branch feature/login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. main 브랜치의 포인터를 1개 이전 커밋(HEAD~1)으로 되감는다&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># --keep을 사용하면 작업 디렉토리의 커밋되지 않은 변경을 유지하면서 안전하게 리셋할 수 있습니다.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git reset --keep HEAD~1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 올바른 브랜치로 전환한다&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout feature/login
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="도해-내부에서-무슨-일이-일어났을까">도해: 내부에서 무슨 일이 일어났을까?
&lt;/h3>&lt;p>Mermaid의 &lt;code>gitGraph&lt;/code>를 사용하여 이때의 브랜치 포인터 이동을 시각화해 봅시다.&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;초기 커밋&amp;#34;
commit id: &amp;#34;버그 수정&amp;#34;
commit id: &amp;#34;실수한 커밋&amp;#34; type: HIGHLIGHT
branch feature/login
checkout feature/login
checkout main
&lt;/pre>
&lt;p>처음에는 &lt;code>main&lt;/code>과 &lt;code>HEAD&lt;/code>가 &amp;ldquo;실수한 커밋&amp;quot;을 가리키고 있었지만, &lt;code>git branch feature/login&lt;/code>에 의해 거기에 새로운 포인터가 만들어집니다. 그 후 &lt;code>git reset&lt;/code>에 의해 &lt;code>main&lt;/code> 포인터만 &amp;ldquo;버그 수정&amp;quot;의 위치로 돌아가는 것입니다. 객체 자체는 아무것도 삭제되지 않았습니다.&lt;/p>
&lt;hr>
&lt;h2 id="5-케이스-스터디-2-이미-푸시한-커밋을-취소하고-싶다">5. 케이스 스터디 2: 이미 푸시한 커밋을 취소하고 싶다!
&lt;/h2>&lt;p>&lt;strong>【상황】&lt;/strong>
심야의 텐션으로 작성한 버그투성이 코드를 커밋하고, 게다가 &lt;code>git push origin main&lt;/code>으로 원격 리포지토리에 공개해 버렸다. 중대한 버그를 깨닫고 사색이 된다.&lt;/p>
&lt;h3 id="해결책-1-역사를-상쇄하는-git-revert-권장안전">해결책 1: 역사를 상쇄하는 &lt;code>git revert&lt;/code> (권장·안전)
&lt;/h3>&lt;p>팀 개발에 있어서, 이미 푸시된 커밋의 이력을 &lt;code>git reset&lt;/code> 등으로 조작하는 것은 엄금입니다. 다른 개발자의 로컬 리포지토리와 정합성이 맞지 않게 됩니다. 올바른 접근 방식은 &lt;strong>&amp;ldquo;실수한 커밋의 변경을 완전히 상쇄하는, 반대의 커밋을 새로 만드는&amp;rdquo;&lt;/strong> 것입니다. 이것이 &lt;code>git revert&lt;/code>입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 최신 커밋을 상쇄하는 커밋을 생성&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git revert HEAD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>main 7f3a8b2&lt;span class="o">]&lt;/span> Revert &lt;span class="s2">&amp;#34;실수한 커밋의 메시지&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="m">1&lt;/span> file changed, &lt;span class="m">1&lt;/span> insertion&lt;span class="o">(&lt;/span>+&lt;span class="o">)&lt;/span>, &lt;span class="m">10&lt;/span> deletions&lt;span class="o">(&lt;/span>-&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 원격으로 푸시&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;커밋 A&amp;#34;
commit id: &amp;#34;커밋 B (실수)&amp;#34;
commit id: &amp;#34;커밋 B 되돌리기&amp;#34; type: REVERSE
&lt;/pre>
&lt;p>역사는 계속 진행되며, 코드의 상태만 원래대로 돌아갑니다.&lt;/p>
&lt;h3 id="해결책-2-역사를-조작하는-git-push---force-with-lease">해결책 2: 역사를 조작하는 &lt;code>git push --force-with-lease&lt;/code>
&lt;/h3>&lt;p>만약 당신이 혼자서만 사용하는 브랜치에 푸시한 직후라면, 역사를 다시 쓰는 것도 용인됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 로컬에서 커밋을 리셋하고 수정&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git reset --hard HEAD~1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git commit -m &lt;span class="s2">&amp;#34;Correct implementation&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 원격의 역사를 강제 덮어쓰기&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git push origin feature/login --force-with-lease
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>--force-with-lease&lt;/code>는 다른 사람의 작업을 실수로 덮어써 버리는 사고를 막기 위한 안전한 강제 푸시입니다.&lt;/p>
&lt;hr>
&lt;h2 id="6-케이스-스터디-3-작업-도중에-다른-브랜치로-전환하고-싶다-stash의-마법">6. 케이스 스터디 3: 작업 도중에 다른 브랜치로 전환하고 싶다 (Stash의 마법)
&lt;/h2>&lt;p>&lt;strong>【상황】&lt;/strong>
&lt;code>feature/A&lt;/code> 브랜치에서 새로운 기능 구현 중, 소스 코드는 아직 컴파일도 통과하지 않는 어중간한 상태. 갑자기 상사로부터 &amp;ldquo;&lt;code>main&lt;/code> 브랜치의 운영 환경에 긴급 버그가 있으니 지금 당장 고쳐줘!&amp;ldquo;라는 지시가 날아왔다.&lt;/p>
&lt;h3 id="해결-방법-git-stash에-의한-대피">해결 방법: &lt;code>git stash&lt;/code>에 의한 대피
&lt;/h3>&lt;p>&lt;code>git stash&lt;/code>는 아직 커밋되지 않은 변경 사항을 임시 영역으로 대피시키는 명령어입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 작업 중인 변경 사항을 대피&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git stash push -m &lt;span class="s2">&amp;#34;WIP: feature A partially implemented&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. main 브랜치로 전환 가능해짐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ...(긴급 버그 수정 작업을 수행하고, 커밋·푸시)...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 작업이 끝나면 원래 브랜치로 돌아감&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout feature/A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. 대피해 두었던 변경 사항을 복원&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git stash pop
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>git stash&lt;/code>를 실행하면 Git은 내부적으로 2개의 특수한 커밋 객체를 생성하고 &lt;code>refs/stash&lt;/code>라는 참조에 저장합니다. 즉, Stash도 결국 &amp;ldquo;이름이 없는 임시 커밋&amp;quot;인 것입니다.&lt;/p>
&lt;hr>
&lt;h2 id="7-케이스-스터디-4-공포의-detached-head-상태">7. 케이스 스터디 4: 공포의 &amp;ldquo;Detached HEAD&amp;rdquo; 상태
&lt;/h2>&lt;p>&lt;strong>【상황】&lt;/strong>
과거 특정 시점의 코드를 확인하고 싶어서 &lt;code>git checkout 9f8a7b6&lt;/code>을 실행했다. 그랬더니 &lt;code>You are in 'detached HEAD' state.&lt;/code>라고 표시되었다. 그대로 커밋했는데, 브랜치를 전환했더니 커밋이 사라져 버렸다!&lt;/p>
&lt;h3 id="detached-head의-메커니즘">Detached HEAD의 메커니즘
&lt;/h3>&lt;p>보통 &lt;code>HEAD&lt;/code>는 &lt;code>refs/heads/main&lt;/code> 등의 브랜치를 가리키고 있습니다. 하지만 특정 커밋을 직접 체크아웃하면, &lt;code>HEAD&lt;/code>가 직접 커밋 객체를 가리키게 됩니다. 이것을 **Detached HEAD(분리된 HEAD)**라고 부릅니다.&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;커밋 A&amp;#34;] --&amp;gt; B[&amp;#34;커밋 B&amp;#34;]
B --&amp;gt; C[&amp;#34;커밋 C&amp;#34;]
C --&amp;gt; D[&amp;#34;커밋 D&amp;#34;]
BranchMain[&amp;#34;브랜치: main&amp;#34;] --&amp;gt; D
HEAD[&amp;#34;HEAD&amp;#34;] --&amp;gt; B
style HEAD fill:#f9f,stroke:#333,stroke-width:4px
&lt;/pre>
&lt;p>이 상태에서 커밋을 쌓아도, 어떤 브랜치도 그 새로운 커밋을 추적해주지 않습니다. 다른 브랜치로 전환하는 순간 새로운 커밋은 미아가 됩니다.&lt;/p>
&lt;h3 id="해결책-새로운-브랜치로-저장하기">해결책: 새로운 브랜치로 저장하기
&lt;/h3>&lt;p>현재 있는 위치에 새로운 브랜치를 생성하면 해결됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 현재 HEAD의 위치에 새로운 브랜치를 만들고 거기로 전환한다&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout -b feature/recovered-work
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;h2 id="8-케이스-스터디-5-병합merge과-리베이스rebase에서의-충돌-해결">8. 케이스 스터디 5: 병합(Merge)과 리베이스(Rebase)에서의 충돌 해결
&lt;/h2>&lt;p>&lt;strong>【상황】&lt;/strong>
&lt;code>git merge&lt;/code>나 &lt;code>git rebase&lt;/code>를 실행했더니 &lt;code>CONFLICT (content)&lt;/code>라고 표시되며 프로세스가 중단되었다.&lt;/p>
&lt;h3 id="병합merge과-리베이스rebase의-차이">병합(Merge)과 리베이스(Rebase)의 차이
&lt;/h3>&lt;ol>
&lt;li>&lt;strong>Merge(병합)&lt;/strong>
2개의 브랜치의 최신 커밋과 공통의 조상을 사용한 3-way merge를 수행하여, 병합 커밋을 생성합니다.&lt;/li>
&lt;li>&lt;strong>Rebase(리베이스)&lt;/strong>
현재 브랜치의 커밋을 임시 저장하고, 대상의 끝에 다시 적용합니다. 역사가 일직선이 됩니다.&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;M1&amp;#34;
commit id: &amp;#34;M2&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;F1&amp;#34;
commit id: &amp;#34;F2&amp;#34;
checkout main
commit id: &amp;#34;M3&amp;#34;
merge feature
&lt;/pre>
&lt;h3 id="충돌-해결법">충돌 해결법
&lt;/h3>&lt;p>충돌이 발생한 파일에는 다음과 같은 마커가 삽입되어 있습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="nx">HEAD&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://api.production.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">=======&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://api.staging.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span> &lt;span class="nx">feature&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">api&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>해결 절차는 극히 간단합니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>마커를 삭제하고 올바른 코드로 수정한다.&lt;/strong>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NODE_ENV&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;production&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?&lt;/span> &lt;span class="s2">&amp;#34;https://api.production.example.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://api.staging.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>&lt;strong>해결한 파일을 스테이징에 추가한다.&lt;/strong>
&lt;code>git add&lt;/code>는 &amp;ldquo;충돌을 해결했다는 것을 Git에 알리는&amp;rdquo; 역할을 합니다.
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ git add index.js
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>&lt;strong>프로세스를 완료시킨다.&lt;/strong>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 병합의 경우&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git commit -m &lt;span class="s2">&amp;#34;Resolve merge conflict in index.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 리베이스의 경우&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>패닉에 빠진다면 &lt;code>$ git merge --abort&lt;/code> 또는 &lt;code>$ git rebase --abort&lt;/code>로 언제든지 중단할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="9-케이스-스터디-6-커밋-내역이-엉망진창-git-rebase--i">9. 케이스 스터디 6: 커밋 내역이 엉망진창! &lt;code>git rebase -i&lt;/code>
&lt;/h2>&lt;p>&lt;strong>【상황】&lt;/strong>
&amp;ldquo;오타 수정&amp;rdquo;, &amp;ldquo;역시 수정&amp;rdquo;, &amp;ldquo;테스트 추가&amp;rdquo; 등 자잘한 커밋이 대량으로 발생해 버렸다. 이대로 &lt;code>main&lt;/code>에 병합하면 내역이 지저분해진다.&lt;/p>
&lt;h3 id="해결책-인터랙티브-리베이스">해결책: 인터랙티브 리베이스
&lt;/h3>&lt;p>&lt;code>git rebase -i&lt;/code> (interactive)를 사용하면 과거 커밋의 순서를 바꾸거나, 여러 커밋을 하나로 합치거나(squash), 커밋 메시지를 수정하거나 할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 최근 3개의 커밋을 정리한다&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git rebase -i HEAD~3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>에디터가 열리며 다음과 같이 표시됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick 1a2b3c4 오타 수정
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick 2b3c4d5 역시 수정
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick 3c4d5e6 테스트 추가
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이것을 다음과 같이 고쳐 씁니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick 1a2b3c4 기능 X 구현
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash 2b3c4d5 역시 수정
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash 3c4d5e6 테스트 추가
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>저장하고 닫으면 이 3개의 커밋이 하나로 아름답게 통합됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="10-케이스-스터디-7-버그가-언제-유입되었는지-모르겠다-git-bisect">10. 케이스 스터디 7: 버그가 언제 유입되었는지 모르겠다! &lt;code>git bisect&lt;/code>
&lt;/h2>&lt;p>&lt;strong>【상황】&lt;/strong>
현재 &lt;code>main&lt;/code> 브랜치에는 버그가 있지만, 1달 전 릴리스 때는 정상이었다. 어느 커밋에서 버그가 섞여 들어왔는지 특정하고 싶지만, 커밋이 100개가 넘어서 수작업으로는 무리!&lt;/p>
&lt;h3 id="해결책-이진-탐색을-통한-버그-특정">해결책: 이진 탐색을 통한 버그 특정
&lt;/h3>&lt;p>Git에는 버그가 유입된 커밋을 수학적인 이진 탐색(Binary Search)으로 찾아내는 도구가 내장되어 있습니다. 계산량은 $\mathcal{O}(\log N)$이므로, 1000개의 커밋이 있어도 약 10번의 테스트로 특정할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 탐색을 시작&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 현재 커밋에는 버그가 있다 (bad)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect bad
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1달 전(예를 들어 해시가 a1b2c3d)은 정상이었다 (good)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect good a1b2c3d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Git이 자동으로 중간의 커밋을 체크아웃하므로 테스트를 실행한다&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 테스트가 성공했다면:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect good
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 테스트가 실패했다면:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect bad
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이것을 반복하는 것만으로 Git이 &amp;ldquo;이 커밋이 첫 번째 Bad 커밋입니다&amp;quot;라고 정확히 알려줍니다. 끝나면 &lt;code>$ git bisect reset&lt;/code>으로 원래 상태로 돌아갑니다.&lt;/p>
&lt;hr>
&lt;h2 id="11-궁극의-안전망-git-reflog">11. 궁극의 안전망: &lt;code>git reflog&lt;/code>
&lt;/h2>&lt;p>Git에서의 모든 &amp;ldquo;실수&amp;quot;에 대한 최종 오의가 &lt;code>git reflog&lt;/code>입니다. Git은 로컬에서의 조작 이력(HEAD의 이동 이력)을 일정 기간 모두 기록하고 있습니다. 브랜치를 삭제해 버리거나, 잘못된 리셋을 해버리거나 해도, &lt;code>git reflog&lt;/code>로 과거의 해시를 찾아내어 거기에 &lt;code>git reset --hard&lt;/code>하는 것만으로 복구 가능합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ git reflog
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">9f8a7b6 &lt;span class="o">(&lt;/span>HEAD -&amp;gt; main&lt;span class="o">)&lt;/span> HEAD@&lt;span class="o">{&lt;/span>0&lt;span class="o">}&lt;/span>: commit: Add new feature
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1a2b3c4 HEAD@&lt;span class="o">{&lt;/span>1&lt;span class="o">}&lt;/span>: reset: moving to HEAD~1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="12-마치며">12. 마치며
&lt;/h2>&lt;p>Git 초보자가 빠지기 쉬운 실수와 그 배경에 있는 Git의 구조, 그리고 해결 방법에 대해 매우 상세하게 해설해 보았습니다. 잘못된 브랜치에 커밋, 이미 푸시된 커밋 취소, Stash의 활용, Detached HEAD에서의 생환, 그리고 충돌 해결. 이 모든 것에서 중요한 것은 &amp;ldquo;Git이 이면에서 어떤 객체와 포인터를 조작하고 있는가&amp;quot;를 이미지화하는 것입니다.&lt;/p>
&lt;p>수식으로 표현되는 엄격한 Diff 알고리즘에 의해 파일의 차이가 계산되고, 암호학적인 해시 함수에 의해 역사의 정합성이 담보되고 있습니다. 이 아름다운 설계 사상을 이해한다면, Git은 결코 &amp;ldquo;정체를 알 수 없는 블랙박스&amp;quot;가 아니라 당신의 소스 코드를 굳건히 지키는 최강의 방패라는 것을 알 수 있을 것입니다.&lt;/p>
&lt;p>다음번에 &amp;ldquo;아차!&amp;rdquo; 하는 생각이 들 때는, 당황해서 터미널을 닫지 말고 심호흡을 한 뒤 &lt;code>git status&lt;/code>를 입력해 보세요. Git은 반드시 복구를 위한 힌트를 당신에게 제시해 줄 것입니다.&lt;/p></description></item></channel></rss>