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. 작은 팀을 위한 권장 작업 흐름
- 각 작업마다 항상
main에서 새 branch를 생성합니다:git switch -c feature/xxx. - 작은 commit을 자주, 명확한 메시지와 함께 합니다.
- Pull Request를 생성하기 전에 branch를 최신
main으로 업데이트합니다:git switch feature/add-payment git fetch origin git merge origin/main - 충돌이 있으면 로컬 머신에서 해결하고 테스트를 다시 실행합니다.
- 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하기 전에 항상 테스트를 실행하세요.