Git 핵심 정리

Git은 소스 코드의 변경 이력을 관리하는 분산 버전 관리 시스템이다.

파일을 수정하고 저장하는 것만으로는 이전 상태를 안전하게 복구하기 어렵다. 여러 기능을 동시에 개발하거나 여러 개발자가 같은 코드를 수정하기 시작하면 어떤 변경이 언제 발생했고, 누가 무엇을 수정했으며, 특정 변경을 되돌리려면 어디까지 되돌려야 하는지 관리할 필요가 생긴다.

Git은 이 문제를 변경 이력과 작업 상태를 관리하는 것으로 해결한다.

Git을 이해할 때 중요한 것은 commit, branch, merge, rebase, remote 같은 명령어를 외우는 것이 아니다. Git이 어떤 상태를 관리하고, 변경사항이 어떤 흐름으로 저장되는지를 이해하는 것이 먼저다.


Git의 기본 구조

Git의 기본적인 작업 흐름은 다음과 같다.

Working Directory -> git add -> Staging Area -> git commit -> Repository

개발자가 파일을 수정하면 Working Directory에 변경사항이 생긴다.

git add를 실행하면 원하는 변경사항을 Staging Area에 올린다.

그리고 git commit을 실행하면 Staging Area의 변경사항이 하나의 Commit으로 Repository에 기록된다.

이 구조를 이해하면 Git 명령어 대부분이 어디에 영향을 주는지 이해할 수 있다.


Working Directory

현재 개발자가 실제로 작업하고 있는 파일 상태다.

예를 들어

Player.cpp / Player.h / Inventory.cpp

를 수정했다면 아직 Commit하지 않은 변경사항은 Working Directory에 존재한다.

Git은 이 변경사항을 기존 Commit과 비교할 수 있다.

git status

를 사용하면 현재 작업 상태를 확인할 수 있다.


Staging Area

Staging Area는 다음 Commit에 포함할 변경사항을 임시로 선택하는 영역이다.

예를 들어 다음과 같이 여러 파일을 수정했다고 하자.

Player.cpp / Player.h / Inventory.cpp / README.md

그중 Player 관련 변경만 Commit하고 싶다면

git add Player.cpp Player.h

처럼 필요한 파일만 Stage할 수 있다.

이후

git commit -m "Update player system"

을 실행하면 Stage된 변경사항만 Commit된다.

따라서 Commit은 단순히 “현재 폴더의 모든 변경사항을 저장하는 것”이 아니다.

Staging Area에 올려놓은 변경사항을 하나의 역사적 상태로 기록하는 것이다.


Commit

Commit은 Git에서 가장 중요한 단위다.

하나의 Commit에는 특정 시점의 프로젝트 상태와 그 상태로 변경된 내용을 나타내는 정보가 들어 있다.

Commit A -> Commit B -> Commit C

개발하면서 Commit이 계속 쌓이면 프로젝트의 변경 이력이 된다.

git log

을 사용하면 Commit history를 확인할 수 있다.

Commit에는 일반적으로 다음과 같은 정보가 존재한다.

Commit -> 변경 내용 / 작성자 / 시간 / 메시지 / 부모 Commit

Git은 이 Commit들을 연결해서 프로젝트의 역사를 구성한다.


Commit은 저장 지점이다

Commit을 단순히 “파일 저장”이라고 생각하면 Git을 잘못 이해하게 된다.

Commit은 의미 있는 변경 단위로 만드는 것이 좋다.

예를 들어

Commit 1 -> Player 이동 시스템 추가
Commit 2 -> Player 공격 시스템 추가
Commit 3 -> 공격 시스템 버그 수정

처럼 만들면 변경 이력을 이해하기 쉽다.

반대로

Commit 1 -> 이것저것 수정

처럼 만들면 나중에 특정 변경을 찾거나 되돌리기 어려워진다.

Git을 잘 사용하는 개발자는 Commit을 많이 만드는 사람이 아니라 의미 있는 단위로 이력을 만드는 사람에 가깝다.


Branch

Branch는 특정 Commit을 기준으로 독립적인 작업 흐름을 만드는 기능이다.

예를 들어

A -> B -> C

라는 Commit history가 있다고 하자.

여기서 새로운 기능을 개발하기 위해 Branch를 만들면

A -> B -> C -> D -> E -> F
           / 
          Feature Branch

처럼 별도의 작업 흐름을 만들 수 있다.

실제로는 Branch가 별도의 파일 복사본을 만드는 개념이라기보다 특정 Commit을 가리키는 참조에 가깝다.

그래서 Branch를 만드는 비용이 매우 작다.

git branch feature/inventory
git switch feature/inventory

또는

git switch -c feature/inventory

처럼 새로운 Branch를 만들고 이동할 수 있다.


Branch를 사용하는 이유

Branch의 핵심 목적은 작업을 격리하는 것이다.

예를 들어 현재 main에서 안정적인 게임 버전을 유지하고 있다고 하자.

main -> A -> B -> C

여기에서 새로운 인벤토리 기능을 개발한다면

main -> A -> B -> C / feature/inventory -> D -> E -> F

처럼 별도의 Branch에서 작업할 수 있다.

그러면 인벤토리 개발이 진행되는 동안 기존 main의 상태를 유지할 수 있다.

이것이 Git Branch의 핵심적인 역할이다.


Merge

작업이 끝난 Branch를 다른 Branch에 합치는 작업이 Merge다.

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

개념적으로는 Feature Branch에서 작업한 내용을 main에 합치는 것이다.

git switch main
git merge feature/inventory

Merge의 장점은 기존 Branch의 Commit history를 유지하면서 두 작업 흐름을 결합할 수 있다는 점이다.


Merge Conflict

두 Branch에서 같은 코드를 서로 다르게 수정하면 Git이 어떤 변경을 선택해야 할지 판단하지 못할 수 있다.

예를 들어

player->Move(speed);

를 한 Branch에서는

player->Move(moveSpeed);

로 변경하고 다른 Branch에서는

player->Move(currentSpeed);

로 변경했다고 하자.

Git 입장에서는 둘 중 어느 것이 올바른지 알 수 없다.

이런 상황이 Conflict다.

Branch A -> 같은 부분 수정 <- Branch B

Conflict가 발생하면 개발자가 직접 어떤 코드를 남길지 결정해야 한다.

Git은 코드의 의미를 판단하는 시스템이 아니다.

변경 이력을 관리할 수는 있지만, 비즈니스 로직의 정답을 판단하지는 않는다.


Rebase

Rebase도 Branch의 변경 이력을 정리하는 데 사용된다.

예를 들어

A -> B -> C / D -> E

라는 상황에서 Feature Branch의 기준을 최신 C로 옮길 수 있다.

A -> B -> C -> D' -> E'

Feature Branch의 Commit을 새로운 기준 위에 다시 적용하는 방식이다.

그래서 Commit의 Hash도 새로 만들어진다.

D -> D'
E -> E'

Rebase의 장점은 Commit history를 선형적으로 정리하기 좋다는 것이다.

다만 Rebase는 기존 Commit을 재작성한다.

따라서 이미 여러 사람이 공유하고 있는 Branch에서 무분별하게 Rebase하면 문제가 발생할 수 있다.


Merge와 Rebase

둘 다 Branch를 통합하는 데 사용할 수 있지만 목적과 결과가 다르다.

Merge -> A -> B -> C -> M / D -> E
Rebase -> A -> B -> C -> D' -> E'

Merge는 실제 Branch가 갈라지고 합쳐진 이력을 그대로 남긴다.

Rebase는 Feature Branch의 Commit을 최신 기준 위에 다시 배치해서 history를 선형적으로 만든다.

개인적으로 Git을 사용할 때는 다음 정도로 구분하면 충분하다.

Merge -> 실제 작업 이력을 보존하고 싶을 때
Rebase -> Commit history를 정리하고 싶을 때

HEAD

Git을 이해하려면 HEAD도 알아야 한다.

HEAD는 현재 내가 바라보고 있는 Commit을 나타내는 참조다.

일반적으로 현재 Branch를 가리키고 있고, Branch가 다시 특정 Commit을 가리킨다.

HEAD -> main -> Commit C

Branch를 이동하면 HEAD도 함께 이동한다.

git switch feature

을 실행하면

HEAD -> feature -> Commit F

와 같은 상태가 된다.

그래서 git status에서 현재 Branch가 무엇인지 확인하는 것이 중요하다.


Detached HEAD

특정 Commit을 직접 Checkout하거나 Switch하면 Branch가 아닌 Commit 자체를 바라보는 상태가 될 수 있다.

HEAD -> Commit C / main -> Commit E

이것을 Detached HEAD 상태라고 한다.

이 상태에서 새로운 Commit을 만들어도 어떤 Branch에서도 해당 Commit을 가리키지 않을 수 있다.

특정 과거 버전을 확인하거나 디버깅할 때는 유용하지만, 작업을 계속할 목적이라면 새로운 Branch를 만드는 것이 안전하다.


Remote Repository

Git 자체는 로컬에서 동작하는 분산 버전 관리 시스템이다.

따라서 Git을 설치했다고 해서 GitHub 같은 서버가 필요한 것은 아니다.

Local Repository

만으로도 Commit, Branch, Merge, Rebase 등의 작업을 할 수 있다.

여기에 원격 Repository를 연결하면 다른 개발자와 변경사항을 공유할 수 있다.

Local Repository -> push -> Remote Repository -> fetch / pull -> Local Repository

GitHub, GitLab, Bitbucket 등이 대표적인 Git 호스팅 서비스다.


Push

push는 로컬 Repository의 Commit을 Remote Repository에 전송하는 작업이다.

git push origin main

개념적으로 보면

Local A -> B -> C -> push -> Remote A -> B -> C

가 된다.

주의할 점은 push가 파일을 단순 업로드하는 것이 아니라 Git Commit을 원격 Repository에 전달하는 작업이라는 것이다.


Fetch

fetch는 Remote Repository의 변경사항을 로컬로 가져오지만 현재 작업 파일에는 바로 적용하지 않는다.

git fetch origin

예를 들어 Remote에 새로운 Commit이 생겼다면

Local -> A -> B / Remote -> A -> B -> C

Fetch 후에는 로컬이 Remote의 Commit 정보를 알게 된다.

Local Branch -> A -> B / origin/main -> A -> B -> C

하지만 현재 Working Directory가 자동으로 C 상태가 되는 것은 아니다.

이것이 pull과의 중요한 차이다.


Pull

pull은 일반적으로 Remote의 변경사항을 가져온 다음 현재 Branch에 통합하는 작업이다.

개념적으로는

git pull -> git fetch + git merge

와 비슷하게 생각할 수 있다.

다만 실제 동작은 설정에 따라 Rebase 방식으로 구성할 수도 있다.

git pull

은 편리하지만 무조건 사용하는 습관은 좋지 않다.

원격에 어떤 변경이 있는지 먼저 확인하고 싶다면

git fetch

후 상태를 확인하는 것이 더 명확하다.


Origin

Git에서 origin은 특별한 명령어가 아니다.

Remote Repository에 붙인 기본적인 이름일 뿐이다.

origin -> https://github.com/example/project.git

다음과 같이 다른 이름을 사용할 수도 있다.

git remote add upstream ...

그러면

origin -> 내가 작업하는 원격 Repository
upstream -> 원본 Repository

처럼 역할을 나눌 수 있다.

Fork 기반의 오픈소스 개발에서 자주 볼 수 있는 구조다.


Reset

Reset은 현재 Branch의 상태를 특정 Commit 기준으로 이동시키는 작업이다.

예를 들어

A -> B -> C -> D

에서 D를 없애고 C로 돌아가고 싶다면 Reset을 사용할 수 있다.

A -> B -> C

다만 Reset은 옵션에 따라 동작이 크게 달라진다.

Soft

git reset --soft HEAD~1

Commit만 되돌리고 변경 내용은 Staging Area에 남긴다.

Mixed

git reset HEAD~1

Commit과 Staging 상태를 되돌리지만 Working Directory의 파일 변경은 유지한다.

Hard

git reset --hard HEAD~1

Commit뿐만 아니라 Working Directory까지 해당 상태로 되돌린다.

--hard는 실제 작업 내용을 잃을 수 있으므로 주의해서 사용해야 한다.


Revert

이미 공유된 Commit을 되돌릴 때는 reset보다 revert가 적합한 경우가 많다.

예를 들어

A -> B -> C

에서 C의 변경을 취소하고 싶다면 Revert는 새로운 Commit을 만든다.

A -> B -> C -> R

RC의 변경을 반대로 적용한 Commit이다.

따라서 기존 history를 지우지 않는다.

공유된 Branch에서는 이런 방식이 안전하다.

Reset -> history를 이동하거나 재작성
Revert -> history를 유지하면서 변경을 취소

Stash

작업 중인데 다른 Branch로 급하게 이동해야 하는 상황이 있다.

아직 Commit하기에는 작업이 덜 끝났지만 현재 변경사항을 잠시 치워두고 싶다면 Stash를 사용할 수 있다.

git stash

그러면 현재 변경사항을 임시로 저장하고 Working Directory를 깨끗하게 만들 수 있다.

나중에

git stash pop

으로 다시 가져올 수 있다.

다만 Stash를 장기적인 작업 저장소처럼 사용하는 것은 좋지 않다.

가능하면 의미 있는 작업 단위로 Commit을 만들어두는 것이 낫다.


Cherry-pick

특정 Branch의 Commit 하나만 가져오고 싶을 때 사용하는 것이 Cherry-pick이다.

예를 들어

main -> A -> B -> C / feature -> D -> E

에서 D의 버그 수정만 main에 가져오고 싶다면

git cherry-pick D

를 사용할 수 있다.

그러면

main -> A -> B -> C -> D'

처럼 해당 변경만 새로운 Commit으로 적용된다.

Hotfix를 다른 Branch에 전달할 때 유용하다.


Tag

Tag는 특정 Commit에 이름을 붙이는 기능이다.

A -> B -> C -> D / v1.0.0 -> C

예를 들어 Release 버전을 표시할 때 사용할 수 있다.

git tag v1.0.0

Branch가 계속 이동하는 참조라면 Tag는 특정 시점을 표시하는 용도로 많이 사용한다.

Branch -> 계속 이동
Tag -> 특정 버전을 표시

게임 빌드나 서버 배포 버전을 관리할 때도 유용하다.


Git의 핵심 명령어 흐름

실제로 자주 사용하는 기본 흐름은 이 정도다.

git status -> git add . -> git commit -m "Add inventory system" -> git push origin feature/inventory

다른 사람의 변경사항을 가져올 때는

git fetch origin

로 원격 상태를 확인한 다음 Merge나 Rebase를 선택할 수 있다.

작업을 시작할 때는

git switch main -> git pull -> git switch -c feature/inventory

처럼 최신 상태의 Branch에서 새로운 작업 Branch를 만드는 방식이 일반적이다.


Git을 잘 사용하는 기준

Git을 잘 사용한다는 것은 명령어를 많이 아는 것이 아니다.

중요한 것은 변경 이력을 관리 가능한 상태로 유지하는 것이다.

예를 들어 Commit을 다음과 같이 구성하는 편이 좋다.

Add player movement -> Fix player movement collision -> Add player attack -> Add attack cooldown -> Fix attack cooldown bug

각 Commit이 하나의 의미를 갖는다.

그러면 특정 기능을 찾거나 버그가 발생했을 때 변경 이력을 따라가기 쉽다.

반대로

Update -> Fix -> test -> asdf -> final -> final2 -> really final

같은 Commit history는 Git을 사용하고 있어도 이력 관리라는 목적을 제대로 달성하지 못한다.


Git과 GitHub는 다르다

이 부분은 처음 Git을 사용할 때 자주 헷갈린다.

Git -> 버전 관리 시스템
GitHub -> Git Repository를 호스팅하는 서비스

Git은 로컬 컴퓨터에서도 동작한다.

GitHub는 Git Repository를 원격에 저장하고 협업할 수 있도록 여러 기능을 제공한다.

GitLab이나 Bitbucket도 같은 범주의 서비스다.

따라서

Git != GitHub

다.

GitHub 없이도 Git을 사용할 수 있다.


Git을 사용하는 개발 Workflow

개인 프로젝트라면 단순하게 사용할 수 있다.

작업 시작 -> Branch 생성 -> 코드 작성 -> 테스트 -> Commit -> Push

팀 개발에서는 여기에 Code Review 과정이 추가된다.

main -> Feature Branch -> 개발 -> Commit -> Push -> Pull Request -> Code Review -> Merge -> main

이 과정에서 Git은 단순한 백업 도구가 아니라 협업 과정에서 변경사항을 추적하고 통합하는 기반 시스템이 된다.


Git과 CI/CD

Git은 CI/CD와도 자연스럽게 연결된다.

예를 들어 개발자가 Push를 하면

git push -> CI Server -> Build / Test / Static Analysis -> 성공 -> Deploy

같은 자동화 Workflow를 구성할 수 있다.

GitHub Actions, GitLab CI/CD 같은 시스템이 대표적이다.

이렇게 되면 Git Commit과 Branch가 단순한 코드 저장 단위를 넘어 빌드와 배포를 발생시키는 이벤트가 된다.


Git에서 중요한 것은 상태와 이력이다

Git을 처음 배울 때는 명령어부터 외우기 쉽다.

add / commit / push / pull / merge / rebase / reset / revert / stash

하지만 명령어만 외우면 상황이 조금만 복잡해져도 헷갈린다.

먼저 현재 상태를 보는 것이 중요하다.

Working Directory -> Staging Area -> Local Repository -> Remote Repository

그리고 Commit history가 어떻게 연결되어 있는지 본다.

A -> B -> C -> D / E -> F

그다음 지금 내가 원하는 것이 무엇인지 판단한다.

변경사항 저장 -> Commit
작업 분리 -> Branch
Branch 통합 -> Merge / Rebase
원격에 공유 -> Push
원격 변경사항 확인 -> Fetch
원격 변경사항 통합 -> Pull
특정 변경 취소 -> Revert
과거 상태로 이동 -> Reset
특정 Commit만 가져오기 -> Cherry-pick

이렇게 생각하면 Git 명령어가 목적에 따라 자연스럽게 연결된다.


정리

Git은 파일을 백업하는 프로그램이 아니다.

프로젝트의 변경 이력을 Commit이라는 단위로 기록하고, Branch를 통해 작업을 분리하고, Merge와 Rebase를 통해 변경사항을 통합하는 버전 관리 시스템이다.

핵심 구조는 다음 정도로 정리할 수 있다.

Working Directory -> Staging Area -> Commit -> Branch -> Remote Repository

그리고 실제 개발에서는

Branch 생성 -> 작업 -> Commit -> Push -> Review -> Merge

라는 흐름을 반복한다.

Git을 제대로 이해하면 reset, revert, rebase, cherry-pick 같은 명령어도 단순 암기가 필요하지 않다. 현재 Repository가 어떤 상태이고, 어떤 이력을 만들고 싶은지를 판단하면 필요한 명령어가 결정된다.

결국 Git의 본질은 하나다.

코드의 현재 상태만 관리하는 것이 아니라, 코드가 어떻게 현재 상태에 도달했는지를 관리하는 것.