Git 기초: commit, branch, merge

1. 개요

Git은 거의 모든 프로젝트에서 사용하는 버전 관리 도구지만, 많은 초보자들은 몇 가지 명령어만 기억하고 원리를 이해하지 못합니다. 이 글에서는 세 가지 핵심 개념을 살펴봅니다: commit(스냅샷 저장), branch(독립적인 작업 분기), merge(분기 병합). 이 글을 읽고 나면, 새로운 분기를 만들고 올바르게 commit하며, merge할 때 충돌을 처리하고, merge 대신 rebase를 언제 사용해야 하는지 알 수 있을 것입니다.

2. Commit – 스냅샷 저장

2.1. Commit이란

각 commit은 특정 시점의 모든 파일 상태를 나타내는 ’스냅샷’이며, 부모 commit을 가리키는 포인터를 포함합니다. Git은 diff를 저장하지 않고 스냅샷을 저장합니다 — 이것이 branch/merge를 빠르고 효율적으로 만듭니다.

# 저장소를 초기화하고 첫 번째 commit 수행
git init order-service
cd order-service
echo "# Order Service" > README.md
git add README.md
git commit -m "docs: Order Service용 README 추가"

설명:

  • git init은 새 저장소를 생성하고, 숨겨진 .git 디렉토리를 만듭니다.
  • git add README.md는 파일을 staging area(commit 대기 영역)에 추가합니다.
  • git commit -m "..."은 스냅샷을 생성하고 변경 사항을 설명하는 메시지를 붙입니다.

히스토리 확인:

git log --oneline
a1b2c3d docs: Order Service용 README 추가

2.2. 좋은 commit 메시지 작성

각 commit은 ‘이 변경이 무엇을 하는가, 왜 하는가’ 질문에 답해야 합니다. 널리 사용되는 규칙(conventional commits):

feat: 주문 생성 API 추가
fix: 쿠폰 적용 시 총액 계산 오류 수정
refactor: OrderValidator를 OrderService에서 분리

주의: "update" 또는 "fix bug"와 같은 모호한 commit 메시지를 피하세요. 몇 개월 후 git log를 다시 보면 그 commit이 무엇을 수정했는지 알 수 없을 것입니다.

2.3. git add -p로 부분 commit하기

한 파일에 서로 무관한 여러 변경사항이 있을 때는 한 번에 모두 commit하지 마세요.

git add -p src/main/java/com/nonladev/OrderService.java

Git은 각 ‘hunk’(변경 섹션)을 stage할지 묻고, 이를 통해 작은 commit으로 나누어 검토하고 필요시 revert하기가 쉬워집니다.

3. Branch – 여러 분기에서 독립적으로 작업

3.1. Branch 생성 및 전환

Branch는 특정 commit을 가리키는 가벼운 포인터일 뿐입니다. 새 branch를 생성해도 전체 코드를 복사하지 않고, 단지 하나의 포인터만 추가됩니다.

# 새 branch를 생성하고 그것으로 전환
git branch feature/add-payment
git checkout feature/add-payment

# 또는 위의 두 명령을 하나로 통합 (Git ≥ 2.23)
git switch -c feature/add-payment

설명:

  • git branch <이름>은 branch를 생성하지만 현재 branch에 남아있습니다.
  • git checkout <이름> 또는 git switch <이름>은 HEAD를 해당 branch로 이동합니다.
  • git switch의 -c는 생성과 전환을 동시에 수행하는 단축키입니다.

3.2. Branch 목록 확인

git branch -v
* feature/add-payment  d4e5f6a PaymentService skeleton 추가
  main                 a1b2c3d docs: Order Service용 README 추가

* 기호는 현재 branch(HEAD가 위치한 곳)를 나타냅니다.

3.3. 규칙에 따른 branch 이름 지정

branch를 명확하게 이름 지으면 팀 모두가 그 branch의 용도를 쉽게 알 수 있습니다:

feature/add-payment       # 새로운 기능 추가
fix/order-total-wrong     # 버그 수정
refactor/extract-validator
hotfix/prod-null-pointer

3.4. 완료 후 branch 삭제

# merge된 로컬 branch 삭제
git branch -d feature/add-payment

# merge되지 않은 branch 삭제 (강제 삭제, commit 손실 주의)
git branch -D feature/add-payment

-d(소문자)는 branch가 아직 merge되지 않았으면 경고를 표시하므로 더 안전합니다.

4. Merge – 분기 병합

4.1. Fast-forward merge

main에 branch를 분리한 이후로 새로운 commit이 없으면, Git은 단지 main 포인터를 feature branch의 마지막 commit으로 이동시킵니다 — 이것을 fast-forward라고 부릅니다.

git switch main
git merge feature/add-payment
Updating a1b2c3d..d4e5f6a
Fast-forward
 PaymentService.java | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

새로운 merge commit이 생성되지 않으며, 히스토리는 여전히 직선입니다.

4.2. Merge commit (3-way merge)

두 branch 모두 분리 지점 이후로 고유한 commit을 가지고 있으면, Git은 두 개의 부모를 가진 merge commit을 생성하여 두 branch의 내용을 병합합니다.

git switch main
git commit -am "fix: 아이템 수량 검증 수정"   # main에 새 commit이 있음
git switch feature/add-payment
git switch main
git merge feature/add-payment
Merge made by the 'recursive' strategy.
 PaymentService.java | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

Git은 기본 메시지 Merge branch 'feature/add-payment'와 함께 자동으로 merge commit을 생성합니다.

4.3. Merge 시 충돌 처리

충돌은 두 branch가 같은 코드 영역을 수정할 때 발생합니다. 예를 들어 OrderService 클래스:

// main에서
public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        return order.getSubtotal().multiply(BigDecimal.valueOf(1.1)); // 10% 세금 추가
    }
}
// feature/add-payment에서
public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        return order.getSubtotal().subtract(order.getDiscount()); // 할인 차감
    }
}

merge할 때, Git은 어느 줄을 유지할지 자동으로 결정할 수 없으므로, 파일에 marker가 나타납니다:

public class OrderService {
    public BigDecimal calculateTotal(Order order) {
<<<<<<< HEAD
        return order.getSubtotal().multiply(BigDecimal.valueOf(1.1));
=======
        return order.getSubtotal().subtract(order.getDiscount());
>>>>>>> feature/add-payment
    }
}

올바른 로직으로 결합하기 위해 수동으로 수정해야 합니다(예: 먼저 할인을 차감하고 세금을 더함), <<<<<<<, =======, >>>>>>> marker를 삭제한 후:

git add OrderService.java
git commit

주의: 충돌을 해결한 후 commit할 때 -m이 필요 없습니다. Git은 이것이 merge commit임을 명확히 한 기본 메시지로 에디터를 열 것입니다 — 추가 참고사항이 없으면 그 메시지 그대로 두면 됩니다.

4.4. Merge vs Rebase

기준 merge rebase
히스토리 유지, merge commit 포함 히스토리 재작성, 선형 branch
안전성 기존 commit 수정 안 함 기존 commit 수정 (hash 변경)
사용 시기 공개된 branch(main, develop) 개인 branch, PR 생성 전 깔끔한 히스토리 원할 때
# feature branch를 최신 main 위로 rebase
git switch feature/add-payment
git rebase main

주의: 이미 push한 branch를 rebase하거나 다른 사람이 pull하고 있는 branch를 수정하지 마세요 — 그들의 히스토리를 혼란스럽게 만들고 다시 pull할 때 많은 충돌을 야기할 수 있습니다.

5. JUnit으로 merge 시나리오 테스트

Git이 Java 코드는 아니지만, merge 후 비즈니스 로직이 깨지지 않았는지 확인하기 위해 테스트를 작성할 수 있습니다 — 이것이 CI가 항상 merge commit에서 테스트를 실행하는 이유입니다. 위의 4.3에서 충돌을 해결한 후 OrderService의 예제:

public class OrderService {

    public BigDecimal calculateTotal(Order order) {
        BigDecimal afterDiscount = order.getSubtotal().subtract(order.getDiscount());
        return afterDiscount.multiply(BigDecimal.valueOf(1.1));
    }
}
class OrderServiceTest {

    private final OrderService orderService = new OrderService();

    @Test
    void shouldApplyDiscountThenTax() {
        Order order = new Order(
                BigDecimal.valueOf(100),  // subtotal
                BigDecimal.valueOf(10)    // discount
        );

        BigDecimal total = orderService.calculateTotal(order);

        assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(99.0));
    }
}

설명: subtotal 100에서 discount 10을 빼면 90이 남고, 10% 세금을 곱하면 99가 됩니다. 이 테스트는 두 로직(세금과 할인)을 merge할 때 서로 상쇄되지 않음을 보장하는 ‘안전망’ 역할을 합니다.

✓ shouldApplyDiscountThenTax()

6. 작은 팀을 위한 권장 작업 흐름

  1. 각 작업마다 항상 main에서 새 branch를 생성합니다: git switch -c feature/xxx.
  2. 작은 commit을 자주, 명확한 메시지와 함께 합니다.
  3. Pull Request를 생성하기 전에 branch를 최신 main으로 업데이트합니다:
    git switch feature/add-payment
    git fetch origin
    git merge origin/main
  4. 충돌이 있으면 로컬 머신에서 해결하고 테스트를 다시 실행합니다.
  5. Push하고 PR을 생성하면, CI가 main으로 병합하기 전에 merge commit에서 테스트를 실행합니다.

주의: 많은 사람이 있는 팀에서는 main에 직접 commit하지 마세요 — 항상 branch + PR을 통해 review와 CI 확인을 받으세요.

7. 흔한 실수

  • commit 전에 git add를 빠뜨림: 파일을 수정했는데도 Git이 nothing to commit, working tree clean이라고 표시 — 파일이 아직 stage되지 않았기 때문입니다.
  • 잘못된 branch를 merge함: merge 전에 항상 git status와 git branch로 현재 위치를 확인하세요.
  • 충돌 marker가 코드에 남음: <<<<<<<를 삭제하지 않으면 코드가 컴파일되지 않습니다 — 충돌을 해결한 후 항상 즉시 build/test하세요.
  • 공유 branch에 강제 push: rebase 후 공유된 branch에 git push --force를 하면 동료의 히스토리가 삭제됩니다. --force-with-lease만 사용하고 개인 branch에서만 사용하세요.

8. 결론

Commit, branch, merge는 매일 Git으로 사용하는 세 가지 작업입니다. commit을 확실히 이해하면 코드 히스토리가 명확해지고, branch를 사용하면 병렬로 작업할 수 있으며, merge(충돌 해결 방법 포함)를 이해하면 여러 사람의 코드를 병합할 때 자신감을 갖게 됩니다. 작은 commit으로 연습하고, 규칙에 따라 branch를 명명하며, main으로 merge하기 전에 항상 테스트를 실행하세요.

gitversion-controlbranchmergedevops