코드의 타임머신, 버전 관리 시스템(VCS)이 필요한 이유
프로그래밍을 시작한 지 얼마 되지 않았을 때 많은 입문자가 소스 파일을 백업하기 위해 다음과 같은 방식으로 폴더를 복사하곤 합니다.
VoxelGame_20260901VoxelGame_최종VoxelGame_진짜최종VoxelGame_진짜최종_수정본2
이런 수작업 방식은 어떤 파일의 어느 줄이 왜 바뀌었는지 알 수 없으며, 지난주에 잘 돌아가던 코드가 갑자기 동작하지 않을 때 특정 시점으로 안전하게 되돌리기 불가능합니다. 여러 개발자가 하나의 프로젝트를 함께 개발할 때 각자의 파일을 덮어쓰다 코드가 유실되는 대참사가 벌어지기도 합니다.
Git(깃)은 리누스 토르발스가 리눅스 커널 개발을 위해 고안한 **분산 버전 관리 시스템(Distributed Version Control System)**입니다. 소스 코드의 모든 변경 내역을 의미 있는 스냅샷인 **커밋(Commit)**으로 영구 기록하며, 나뭇가지처럼 갈라지는 **브랜치(Branch)**를 통해 기존 기능을 훼손하지 않고 새로운 기능을 안전하게 실험하고 병합할 수 있도록 돕습니다.
Git의 세 가지 영역과 기본 흐름
Git의 동작 메커니즘을 제대로 이해하려면 컴퓨터 안에서 파일이 이동하는 세 가지 논리적 상태 공간을 머릿속에 그릴 수 있어야 합니다.
- 작업 디렉터리 (Working Directory): 우리가 에디터로 코드를 작성하고 수정하는 실제 파일 탐색기 상의 폴더입니다.
- 스테이징 영역 (Staging Area / Index): 다음 커밋에 포함할 변경 사항만을 선별해 올려놓는 중간 준비 구역입니다.
git add명령을 통해 변경된 파일이 이곳에 등록됩니다. - 로컬 저장소 (Local Repository / .git): 스테이징 영역에 올라온 파일들의 스냅샷이 고유한 해시 식별자(SHA-1/SHA-256)와 함께 영구 보관되는 데이터베이스 공간입니다.
git commit명령으로 생성됩니다.
이 세 영역의 흐름을 정리하면 다음과 같습니다.
[Working Directory] ── git add ──> [Staging Area] ── git commit ──> [Local Repository]
(코드 작성 및 수정) (커밋 대상 선별) (영구 스냅샷 기록)
Java 프로젝트를 위한 .gitignore 설정
Git 저장소를 초기화할 때 가장 먼저 챙겨야 하는 파일이 .gitignore입니다. 프로젝트 폴더 안에는 소스 코드뿐만 아니라 컴파일 결과물, 에디터 설정, 로그 파일 등 Git이 추적해서는 안 되는 파일들이 가득합니다.
만약 바이트코드인 .class 파일이나 IntelliJ의 .idea 폴더를 원격 저장소에 올리면, 팀원마다 JDK 경로와 화면 설정이 달라 잦은 충돌이 발생하고 저장소 용량이 무분별하게 비대해집니다.
프로젝트 루트 폴더에 .gitignore 파일을 만들고 다음과 같이 Java 표준 제외 규칙을 작성합니다.
# 컴파일 결과물
*.class
out/
build/
bin/
# 패키징 아카이브
*.jar
*.war
# IntelliJ IDEA 설정
.idea/
*.iml
*.iws
# Eclipse / VS Code 설정
.metadata/
.project
.classpath
.settings/
.vscode/
# 운영체제 임시 파일
.DS_Store
Thumbs.db
저장소 생성부터 첫 커밋까지의 CLI 실무
터미널에서 직접 명령어를 입력하며 프로젝트의 첫 커밋을 생성하는 전형적인 흐름을 살펴보겠습니다.
# 1. 현재 폴더를 Git 저장소로 초기화
git init
# 2. 작업 상태 확인 (어떤 파일이 수정되었고 스테이징되지 않았는지 점검)
git status
# 3. 변경된 파일 중 커밋할 파일을 스테이징 영역에 추가
git add src/
git add .gitignore
# 4. 의미 있는 메시지와 함께 로컬 저장소에 스냅샷 기록
git commit -m "feat: initial java 25 voxel world project setup"
# 5. 커밋 로그 이력 확인
git log --oneline
좋은 커밋 메시지의 원칙
커밋은 단순히 저장 버튼을 누르는 것이 아니라 “소프트웨어의 변경 이유를 남기는 역사책”입니다. 실무에서는 다음과 같은 통일된 규약을 널리 권장합니다.
feat: 새로운 기능 추가 (예:feat: add inventory capacity check logic)fix: 버그 수정 (예:fix: prevent health underflow on monster hit)refactor: 동작 변경 없는 코드 구조 개선 (예:refactor: extract damage calculation method)docs: 문서 수정 (예:docs: update setup guide in README)test: 테스트 코드 추가 또는 보완 (예:test: add unit test for player leveling)
브랜치(Branch)와 안전한 기능 개발
소프트웨어를 운영하거나 기본 기능이 안정된 상태에서 신규 기능을 개발할 때, 메인 코드 줄기(main 브랜치)에 직접 코드를 밀어 넣는 것은 위험천만한 일입니다. 작업 도중 심각한 오류가 생기면 전체 프로그램이 멈추기 때문입니다.
이때 **브랜치(Branch)**를 만들면 메인 코드 줄기와 분리된 작업 공간에서 안전하게 기능을 개발할 수 있습니다.
실습을 위해 voxel 게임에서 장비 내구도와 인벤토리 시스템을 관리하는 핵심 Java 코드를 작성했습니다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
record VoxelItem(String id, String name, int durability) {
public VoxelItem consumeDurability(int amount) {
return new VoxelItem(this.id, this.name, Math.max(0, this.durability - amount));
}
public boolean isBroken() {
return this.durability == 0;
}
}
static class Backpack {
private final int capacity;
private final List<VoxelItem> slots;
public Backpack(int capacity) {
this.capacity = capacity;
this.slots = new ArrayList<>();
}
public boolean addItem(VoxelItem item) {
if (this.slots.size() >= this.capacity) {
return false;
}
this.slots.add(item);
return true;
}
public List<VoxelItem> getItems() {
return Collections.unmodifiableList(this.slots);
}
public void printInventory() {
System.out.println(String.format("=== 배낭 인벤토리 (%d/%d) ===", this.slots.size(), this.capacity));
for (int i = 0; i < this.slots.size(); i++) {
VoxelItem item = this.slots.get(i);
String status = item.isBroken() ? "[파손됨]" : "[내구도:" + item.durability() + "]";
System.out.println(String.format(" [%d] %s %s", i + 1, item.name(), status));
}
}
}
public static void main(String[] args) {
Backpack backpack = new Backpack(4);
VoxelItem pickaxe = new VoxelItem("pickaxe_01", "다이아몬드 곡괭이", 150);
VoxelItem sword = new VoxelItem("sword_01", "철검", 60);
VoxelItem torch = new VoxelItem("torch_01", "횃불", 10);
backpack.addItem(pickaxe);
backpack.addItem(sword);
backpack.addItem(torch);
backpack.printInventory();
System.out.println("\n곡괭이로 암석을 채굴합니다 (내구도 150 소모)...");
VoxelItem brokenPickaxe = pickaxe.consumeDurability(150);
System.out.println("채굴 후 곡괭이 상태: " + (brokenPickaxe.isBroken() ? "파손됨" : "사용 가능"));
}
}
예상 출력
=== 배낭 인벤토리 (3/4) ===
[1] 다이아몬드 곡괭이 [내구도:150]
[2] 철검 [내구도:60]
[3] 횃불 [내구도:10]
곡괭이로 암석을 채굴합니다 (내구도 150 소모)...
채굴 후 곡괭이 상태: 파손됨
브랜치 조작 핵심 명령어
위와 같은 인벤토리 시스템에 “장비 수리 기능”을 추가하려 한다면 다음처럼 브랜치를 분기하여 작업합니다.
# 1. 새 기능 브랜치를 만들고 동시에 전환
git switch -c feature/item-repair
# 2. 코드 수정 후 스테이징 및 커밋
git add src/Main.java
git commit -m "feat: implement anvil item repair logic"
# 3. 메인 브랜치로 다시 복귀
git switch main
# 4. 기능 브랜치의 변경 사항을 메인에 병합(Merge)
git merge feature/item-repair
# 5. 작업이 끝난 기능 브랜치 삭제
git branch -d feature/item-repair
머지 충돌(Merge Conflict)과 해결법
서로 다른 브랜치에서 동일한 파일의 동일한 줄을 서로 다르게 수정하고 병합을 시도하면 Git은 어느 코드가 맞는지 스스로 판단하지 못하고 **충돌(Conflict)**을 선언합니다.
충돌이 발생하면 Git은 소스 파일 내에 다음과 같은 마커를 삽입합니다.
<<<<<<< HEAD
System.out.println("곡괭이로 암석을 채굴합니다!");
=======
System.out.println("곡괭이로 흑요석 블록을 조심스럽게 채굴합니다!");
>>>>>>> feature/item-repair
<<<<<<< HEAD: 현재 내가 머물고 있는 브랜치의 코드입니다.=======: 두 브랜치 간의 경계선입니다.>>>>>>> feature/item-repair: 병합하려 가져온 대상 브랜치의 코드입니다.
개발자는 양쪽 팀원과 의도를 확인한 뒤 마커 기호들을 모두 지우고 올바른 최종 코드 한 줄만 남겨야 합니다. 정리가 끝난 뒤 git add와 git commit을 차례로 실행하면 충돌 해결이 완료됩니다.
GitHub 원격 저장소와 협업(Pull Request)
로컬 저장소의 코드를 인터넷 클라우드에 올려 공유하려면 GitHub(깃허브)과 같은 원격 저장소 호스팅 서비스를 연동해야 합니다.
- 원격 저장소 연결: GitHub에서 새 리포지토리(예:
voxel-java-core)를 생성하고 로컬과 연결합니다.git remote add origin https://github.com/your-id/voxel-java-core.git git branch -M main git push -u origin main - Pull Request (풀 리퀘스트 / PR):
- 개발자는
main에 직접 push하지 않고,feature/potion-system브랜치를 생성해 작업한 뒤 원격으로 push합니다 (git push origin feature/potion-system). - GitHub 웹 화면에서 New Pull Request 버튼을 눌러 “내 브랜치의 변경 사항을 main 브랜치에 합쳐달라”는 요청서를 제출합니다.
- 개발자는
- 코드 리뷰와 머지:
- 팀원들은 변경된 코드의 줄별 diff를 확인하고 질문이나 개선 의견(댓글)을 남깁니다.
- 모든 검토와 테스트가 통과되면 상단의 Merge pull request 버튼을 눌러 최종 반영합니다.
직접 해보기
아래 실습 코드는 모험가가 획득한 전리품(Loot)을 분류하고 가치를 합산하는 프로그램입니다.
import java.util.List;
public class Main {
record Loot(String name, int goldValue, boolean isRare) {}
public static int sumTotalGold(List<Loot> lootList) {
int total = 0;
for (Loot item : lootList) {
total += item.goldValue();
}
return total;
}
public static void main(String[] args) {
List<Loot> drops = List.of(
new Loot("좀비 살점", 2, false),
new Loot("금괴", 50, true),
new Loot("뼈다귀", 5, false),
new Loot("다이아몬드 원석", 120, true)
);
int total = sumTotalGold(drops);
System.out.println("전리품 총 획득 골드: " + total + "G");
}
}
미션 과제
- 터미널에서 위 코드가 있는 폴더로 이동해
git init을 실행하세요. .gitignore파일을 만들고*.class를 등록한 뒤git add .및git commit -m "feat: create initial loot drop system"을 실행하세요.git switch -c feature/rare-bonus명령어로 새 브랜치를 생성하세요.sumTotalGold메서드를 수정하여 희귀 아이템(isRare == true)인 경우 골드를 1.2배(정수 변환)로 계산하도록 수정한 뒤 커밋하세요.- 다시
main브랜치로 전환(git switch main)하여 원본 코드가 그대로 보존되어 있는지 확인하고,git merge feature/rare-bonus를 실행해 성공적으로 병합되는지 확인하세요.