Chapter 16

Git과 GitHub 협업

분산 버전 관리 시스템 Git의 저장소 원리, 커밋·브랜치·충돌 해결과 GitHub 기반 협업(Pull Request) 워크플로우를 Java 예제로 학습합니다.

난이도
초급
예상 학습
60분
Java 기준
Java 25 LTS
최종 검토

전체 22개 Chapter 확장 구조 중 Chapter 16입니다.

기능 구분

정식 Java 25에서도 쓸 수 있는 이전 버전 기능

정식 개발 워크플로우

Git 버전 관리는 Java 25 신규 기능이 아니라 Java 프로젝트에 함께 쓰는 실무 도구입니다.

이 Chapter의 예제는 Preview 또는 Incubator API 실행 옵션 없이 Java 25 LTS 정식 기능 기준으로 읽을 수 있습니다.

Learning goals

오늘 배울 내용

  • Git의 세 가지 영역(작업 디렉터리, 스테이징 영역, 로컬 저장소)의 데이터 흐름 이해하기
  • Java 프로젝트에 필수적인 .gitignore 설정과 의미 있는 커밋 단위 관리하기
  • 브랜치(Branch)를 통한 독립적 기능 개발과 병합(Merge) 충돌 해결하기
  • GitHub 원격 저장소 연동과 Pull Request(PR) 기반의 협업 절차 체득하기

코드의 타임머신, 버전 관리 시스템(VCS)이 필요한 이유

프로그래밍을 시작한 지 얼마 되지 않았을 때 많은 입문자가 소스 파일을 백업하기 위해 다음과 같은 방식으로 폴더를 복사하곤 합니다.

  • VoxelGame_20260901
  • VoxelGame_최종
  • VoxelGame_진짜최종
  • VoxelGame_진짜최종_수정본2

이런 수작업 방식은 어떤 파일의 어느 줄이 왜 바뀌었는지 알 수 없으며, 지난주에 잘 돌아가던 코드가 갑자기 동작하지 않을 때 특정 시점으로 안전하게 되돌리기 불가능합니다. 여러 개발자가 하나의 프로젝트를 함께 개발할 때 각자의 파일을 덮어쓰다 코드가 유실되는 대참사가 벌어지기도 합니다.

Git(깃)은 리누스 토르발스가 리눅스 커널 개발을 위해 고안한 **분산 버전 관리 시스템(Distributed Version Control System)**입니다. 소스 코드의 모든 변경 내역을 의미 있는 스냅샷인 **커밋(Commit)**으로 영구 기록하며, 나뭇가지처럼 갈라지는 **브랜치(Branch)**를 통해 기존 기능을 훼손하지 않고 새로운 기능을 안전하게 실험하고 병합할 수 있도록 돕습니다.

Git의 세 가지 영역과 기본 흐름

Git의 동작 메커니즘을 제대로 이해하려면 컴퓨터 안에서 파일이 이동하는 세 가지 논리적 상태 공간을 머릿속에 그릴 수 있어야 합니다.

  1. 작업 디렉터리 (Working Directory): 우리가 에디터로 코드를 작성하고 수정하는 실제 파일 탐색기 상의 폴더입니다.
  2. 스테이징 영역 (Staging Area / Index): 다음 커밋에 포함할 변경 사항만을 선별해 올려놓는 중간 준비 구역입니다. git add 명령을 통해 변경된 파일이 이곳에 등록됩니다.
  3. 로컬 저장소 (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 addgit commit을 차례로 실행하면 충돌 해결이 완료됩니다.

GitHub 원격 저장소와 협업(Pull Request)

로컬 저장소의 코드를 인터넷 클라우드에 올려 공유하려면 GitHub(깃허브)과 같은 원격 저장소 호스팅 서비스를 연동해야 합니다.

  1. 원격 저장소 연결: 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
  2. Pull Request (풀 리퀘스트 / PR):
    • 개발자는 main에 직접 push하지 않고, feature/potion-system 브랜치를 생성해 작업한 뒤 원격으로 push합니다 (git push origin feature/potion-system).
    • GitHub 웹 화면에서 New Pull Request 버튼을 눌러 “내 브랜치의 변경 사항을 main 브랜치에 합쳐달라”는 요청서를 제출합니다.
  3. 코드 리뷰와 머지:
    • 팀원들은 변경된 코드의 줄별 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");
    }
}

미션 과제

  1. 터미널에서 위 코드가 있는 폴더로 이동해 git init을 실행하세요.
  2. .gitignore 파일을 만들고 *.class를 등록한 뒤 git add .git commit -m "feat: create initial loot drop system"을 실행하세요.
  3. git switch -c feature/rare-bonus 명령어로 새 브랜치를 생성하세요.
  4. sumTotalGold 메서드를 수정하여 희귀 아이템(isRare == true)인 경우 골드를 1.2배(정수 변환)로 계산하도록 수정한 뒤 커밋하세요.
  5. 다시 main 브랜치로 전환(git switch main)하여 원본 코드가 그대로 보존되어 있는지 확인하고, git merge feature/rare-bonus를 실행해 성공적으로 병합되는지 확인하세요.

FAQ

자주 묻는 질문

Git과 GitHub은 정확히 어떻게 다른가요?

Git은 개인 컴퓨터의 로컬 환경에서 소스 코드의 변경 이력을 스냅샷 단위로 추적하는 분산 버전 관리 도구(VCS)이며, GitHub은 이 Git 저장소를 인터넷 클라우드에 호스팅하여 원격 백업과 다자간 코드 리뷰 및 협업 기능을 제공하는 웹 서비스입니다.

Java 프로젝트의 .gitignore 파일에는 어떤 항목들을 넣어야 하나요?

컴파일된 바이트코드 폴더인 out/ 또는 build/, IDE 설정 파일인 .idea/나 *.iml, 그리고 OS 임시 파일(.DS_Store)을 반드시 제외해야 팀원 간 빌드 충돌과 불필요한 파일 커밋을 막을 수 있습니다.

머지 충돌(Merge Conflict)이 일어났을 때의 대처법은 무엇인가요?

같은 파일의 동일한 줄을 두 개발자가 서로 다르게 수정한 경우 발생합니다. 충돌 표시 기호(<<<<<<<, =======, >>>>>>>)를 확인하고 양측의 의도를 조율한 뒤 최종 코드를 남기고 다시 스테이징(git add) 후 커밋하면 됩니다.

작성·편집 ToolPado

콘텐츠 유형 Java 학습 가이드

Java 기준 Java 25 LTS

마지막 검토