Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기
이 글의 핵심
Git 대규모 리팩토링 브랜치 병합 실전 - 머지 충돌 최소화 및 단계적 해결 전략
들어가며
3개월간 진행된 대규모 리팩토링 브랜치를 main에 병합하려는데, 수백 개 파일에서 충돌이 발생했습니다. 이 글에서는 복잡한 머지 충돌을 체계적으로 해결한 실전 사례를 공유합니다.
일상에 빗대면, 오래된 도면과 최신 현장 사진을 한 번에 겹쳐 붙이려는 작업과 비슷합니다. 한 장에 합치려면 어느 층·어느 구역부터 맞출지 나누지 않으면 금방 엉킵니다.
이 글을 읽으면
- 대규모 브랜치 병합 전략을 배웁니다
- 머지 충돌을 최소화하는 방법을 익힙니다
- 복잡한 충돌을 단계적으로 해결하는 기법을 이해합니다
- 병합 후 회귀를 방지하는 테스트 전략을 습득합니다
목차
- 상황: 3개월 리팩토링 브랜치
- 첫 병합 시도와 실패
- 전략 수립: 단계적 병합
- 1단계: main을 리팩토링 브랜치로 머지
- 2단계: 충돌 분류 및 우선순위
- 3단계: 자동 해결 가능한 충돌
- 4단계: 수동 해결이 필요한 충돌
- 5단계: 테스트 및 검증
- 6단계: main으로 최종 병합
- 교훈: 충돌 최소화 전략
- 마무리
1. 상황: 3개월 리팩토링 브랜치
왜 이렇게까지 충돌이 커졌나
배경은 단순합니다. 장기 브랜치 동안 main에는 기능·버그픽스가 계속 들어왔고, 리팩토링 쪽에서는 디렉터리·클래스명까지 크게 움직였습니다. 같은 파일을 양쪽에서 다르게 진화시킨 결과, 병합 시점에 마커가 폭발한 것입니다.
리팩토링 내용
- 브랜치:
refactor/new-architecture - 기간: 2025년 12월 ~ 2026년 3월 (3개월)
- 변경 범위:
- 디렉토리 구조 변경 (
src/→app/) - 클래스 이름 변경 (
UserManager→UserService) - 의존성 주입 패턴 도입
- 테스트 코드 전면 재작성
- 디렉토리 구조 변경 (
main 브랜치 진행 상황
리팩토링 중에도 main 브랜치는 계속 진행:
- 새로운 기능 20개 추가
- 버그 수정 50개
- 의존성 업데이트 10개
2. 첫 병합 시도와 실패
순진한 시도
$ git checkout refactor/new-architecture
$ git merge main
Auto-merging src/user_manager.cpp
CONFLICT (content): Merge conflict in src/user_manager.cpp
Auto-merging src/auth/login.cpp
CONFLICT (content): Merge conflict in src/auth/login.cpp
...
CONFLICT (modify/delete): src/old_module.cpp deleted in HEAD and modified in main
...
Automatic merge failed; fix conflicts and then commit the result.
결과
$ git status | grep "both modified" | wc -l
247 # 247개 파일에서 충돌!
문제: 한 번에 247개 충돌을 해결하는 것은 불가능합니다.
3. 전략 수립: 단계적 병합
전략
- main을 리팩토링 브랜치로 먼저 머지 (역방향)
- 충돌을 카테고리별로 분류
- 자동 해결 가능한 충돌 먼저 처리
- 수동 해결 필요한 충돌은 하나씩
- 테스트 통과 확인
- 최종적으로 리팩토링 브랜치를 main에 머지
왜 역방향 머지인가?
# 방법 A: refactor 브랜치에서 main 머지 (권장)
$ git checkout refactor/new-architecture
$ git merge main
# 충돌 해결 후 refactor 브랜치에서 테스트
# 문제 없으면 main에 머지
# 방법 B: main에서 refactor 머지 (위험)
$ git checkout main
$ git merge refactor/new-architecture
# 충돌 해결 중 실수하면 main이 망가짐!
이유: 리팩토링 브랜치에서 충돌을 해결하면, main은 안전하게 유지됩니다.
4. 1단계: main을 리팩토링 브랜치로 머지
머지 시작
$ git checkout refactor/new-architecture
$ git merge main --no-commit --no-ff
# 충돌 상황 저장
$ git status > conflicts.txt
충돌 파일 분류
$ grep "both modified" conflicts.txt | wc -l
189 # 양쪽 모두 수정
$ grep "deleted by us" conflicts.txt | wc -l
34 # 리팩토링에서 삭제, main에서 수정
$ grep "added by them" conflicts.txt | wc -l
24 # main에서 새로 추가
5. 2단계: 충돌 분류 및 우선순위
충돌 분류
# classify_conflicts.py
import subprocess
conflicts = subprocess.check_output(['git', 'diff', '--name-only', '--diff-filter=U']).decode().splitlines()
categories = {
'rename': [], # 파일명만 변경
'trivial': [], # 자동 해결 가능 (import 순서 등)
'logic': [], # 로직 변경 (수동 해결 필요)
'delete': [], # 삭제 vs 수정
}
for file in conflicts:
if 'test' in file:
categories['trivial'].append(file)
elif file.endswith('.h') or file.endswith('.hpp'):
categories['rename'].append(file)
else:
categories['logic'].append(file)
for cat, files in categories.items():
print(f"{cat}: {len(files)}개")
결과
rename: 45개
trivial: 78개
logic: 66개
delete: 34개
6. 3단계: 자동 해결 가능한 충돌
Import 순서 충돌
<<<<<<< HEAD (refactor/new-architecture)
#include "app/services/user_service.h"
#include "app/utils/logger.h"
=======
#include "src/user_manager.h"
#include "src/logger.h"
>>>>>>> main
해결: 리팩토링 브랜치의 새 경로 사용
$ git checkout --ours src/some_file.cpp
테스트 파일 충돌
테스트 파일은 리팩토링에서 전면 재작성했으므로:
$ git checkout --ours tests/*.cpp
일괄 처리
# trivial 카테고리 일괄 해결
$ for file in $(cat trivial_conflicts.txt); do
git checkout --ours "$file"
git add "$file"
done
7. 4단계: 수동 해결이 필요한 충돌
로직 충돌 예시
<<<<<<< HEAD (refactor/new-architecture)
// 리팩토링: UserService로 변경
class UserService {
std::shared_ptr<Database> db_;
public:
User getUser(int id) {
return db_->query("SELECT * FROM users WHERE id = ?", id);
}
=======
// main: 새 기능 추가 (캐싱)
class UserManager {
std::unordered_map<int, User> cache_;
public:
User getUser(int id) {
if (auto it = cache_.find(id); it != cache_.end()) {
return it->second;
}
auto user = db_->query("SELECT * FROM users WHERE id = ?", id);
cache_[id] = user;
return user;
}
>>>>>>> main
};
해결: 두 변경 모두 반영
// 병합 결과: 리팩토링 + 캐싱
class UserService {
std::shared_ptr<Database> db_;
std::unordered_map<int, User> cache_; // main의 캐싱 기능 추가
public:
User getUser(int id) {
// main의 캐싱 로직 유지
if (auto it = cache_.find(id); it != cache_.end()) {
return it->second;
}
// 리팩토링된 구조 유지
auto user = db_->query("SELECT * FROM users WHERE id = ?", id);
cache_[id] = user;
return user;
}
};
8. 5단계: 테스트 및 검증
단계별 테스트
# 1. 컴파일 확인
$ cmake --build build
# 성공
# 2. 단위 테스트
$ cd build && ctest
# 1234 tests passed, 5 tests failed
# 3. 실패한 테스트 수정
$ gdb --args ./test_user_service
# 캐싱 로직 테스트 추가 필요
# 4. 통합 테스트
$ ./integration_tests.sh
# 성공
# 5. 성능 회귀 테스트
$ ./benchmark.sh
# 성능 저하 없음 확인
머지 커밋 생성
$ git add .
$ git commit -m "Merge branch 'main' into refactor/new-architecture
Resolved 247 conflicts:
- Renamed files: UserManager → UserService
- Preserved new features from main (caching, new APIs)
- Updated tests for new architecture
All tests passing."
9. 6단계: main으로 최종 병합
PR 생성
$ git push origin refactor/new-architecture
$ gh pr create \
--title "Refactor: New architecture with dependency injection" \
--body "$(cat <<'EOF'
## Summary
3개월간 진행한 아키텍처 리팩토링을 main에 병합합니다.
### 주요 변경사항
- 디렉토리 구조 변경 (src/ → app/)
- 의존성 주입 패턴 도입
- UserManager → UserService 등 이름 변경
- 테스트 커버리지 85% → 92%
### 머지 충돌 해결
- 247개 충돌을 단계적으로 해결
- main의 새 기능 (캐싱, 새 API) 모두 반영
- 모든 테스트 통과 확인
### 테스트 결과
- Unit tests: 1234/1234 passed
- Integration tests: 45/45 passed
- Performance: 회귀 없음
EOF
)"
코드 리뷰
팀원들이 주요 충돌 해결 부분을 리뷰:
# 충돌 해결 부분만 보기
$ git diff main...refactor/new-architecture -- src/user_service.cpp
최종 병합
$ git checkout main
$ git merge refactor/new-architecture --no-ff
$ git push origin main
10. 교훈: 충돌 최소화 전략
핵심 교훈
- 자주 동기화: 장기 브랜치는 주기적으로 main을 머지
- 작은 단위로 분할: 가능하면 리팩토링을 여러 PR로 나누기
- 충돌 분류: 자동/수동 해결 구분하여 효율적으로 처리
- 테스트 필수: 충돌 해결 후 반드시 테스트
장기 브랜치 관리 전략
# 매주 main을 리팩토링 브랜치로 머지
$ git checkout refactor/new-architecture
$ git merge main
# 충돌 해결 (작은 단위로)
# 또는 rebase (히스토리를 깔끔하게)
$ git rebase main
# 단, 이미 공유된 브랜치면 rebase 주의
충돌 최소화 팁
-
파일 이동과 수정 분리:
# Commit 1: 파일만 이동 $ git mv src/user_manager.cpp app/user_service.cpp $ git commit -m "Rename: UserManager → UserService" # Commit 2: 내용 수정 $ vim app/user_service.cpp $ git commit -m "Refactor: Apply DI pattern" -
.gitattributes로 머지 전략 설정:# .gitattributes *.lock merge=ours # 락 파일은 우리 것 사용 *.generated.* merge=theirs # 생성 파일은 최신 것 사용 -
충돌 마커 검색:
# 충돌이 남아있는지 확인 $ git diff --check $ grep -r "<<<<<<< HEAD" .
11. 실전 충돌 해결 패턴
패턴 1: 같은 함수 다른 수정
<<<<<<< HEAD
// 리팩토링: 함수명 변경
void UserService::createUser(const UserData& data) {
db_->insert(data);
}
=======
// main: 유효성 검사 추가
void UserManager::createUser(const UserData& data) {
if (!data.isValid()) {
throw std::invalid_argument("Invalid user data");
}
db_->insert(data);
}
>>>>>>> main
해결: 두 변경 모두 반영
void UserService::createUser(const UserData& data) {
if (!data.isValid()) {
throw std::invalid_argument("Invalid user data");
}
db_->insert(data);
}
패턴 2: 파일 이동 + 수정
# 리팩토링: src/user_manager.cpp → app/user_service.cpp
# main: src/user_manager.cpp에 새 메서드 추가
$ git status
deleted by us: src/user_manager.cpp
added by us: app/user_service.cpp
modified by them: src/user_manager.cpp
해결:
# main의 변경사항 확인
$ git show main:src/user_manager.cpp > /tmp/main_version.cpp
$ diff app/user_service.cpp /tmp/main_version.cpp
# 새 메서드를 app/user_service.cpp에 수동 추가
$ vim app/user_service.cpp
# 충돌 해결
$ git rm src/user_manager.cpp
$ git add app/user_service.cpp
패턴 3: Import 경로 충돌
<<<<<<< HEAD
#include "app/services/user_service.h"
#include "app/utils/logger.h"
=======
#include "src/user_manager.h"
#include "src/logger.h"
#include "src/new_feature.h" // main에서 추가
>>>>>>> main
해결: 새 경로로 통일 + 새 기능 추가
#include "app/services/user_service.h"
#include "app/utils/logger.h"
#include "app/features/new_feature.h" // 경로 변환
12. 도구 활용
VS Code 머지 도구
// settings.json
{
"merge-conflict.autoNavigateNextConflict.enabled": true,
"git.mergeEditor": true
}
Git 설정
# 3-way diff 도구 설정
$ git config --global merge.tool vimdiff
$ git config --global merge.conflictstyle diff3
# 충돌 해결 도구 실행
$ git mergetool
충돌 마커 이해
<<<<<<< HEAD (현재 브랜치)
// 리팩토링 브랜치의 코드
||||||| merged common ancestors (공통 조상)
// 원래 코드
=======
// main 브랜치의 코드
>>>>>>> main
마무리
대규모 리팩토링 브랜치 병합은 어렵지만, 체계적인 전략으로 해결할 수 있습니다:
- 역방향 머지로 main을 안전하게 보호
- 충돌 분류로 효율적 처리
- 단계적 해결로 실수 최소화
- 테스트 필수로 회귀 방지
핵심: 한 번에 모든 충돌을 해결하려 하지 말고, 카테고리별로 나누어 처리하세요.
FAQ
Q1. rebase vs merge 중 뭘 써야 하나요?
장기 브랜치는 merge를 권장합니다. rebase는 히스토리를 다시 쓰므로, 이미 공유된 브랜치에서는 위험합니다.
Q2. 충돌이 너무 많으면 어떻게 하나요?
브랜치를 더 작은 단위로 나누는 것을 고려하세요. 예: 디렉토리 구조 변경 → 이름 변경 → 로직 변경을 각각 별도 PR로.
Q3. main에서 새로 추가된 파일은 어떻게 하나요?
리팩토링 브랜치에서 새 구조에 맞게 수정하여 추가하세요.
관련 글
- Git 브랜치와 병합 완벽 가이드
- Git Rebase 마스터하기
- Git 협업 워크플로우
실전 체크리스트
대규모 병합 체크리스트
- 백업 브랜치 생성
- 충돌 개수 파악
- 충돌 분류 (자동/수동)
- 자동 해결 가능한 충돌 먼저 처리
- 수동 충돌 하나씩 해결
- 컴파일 확인
- 테스트 실행
- 코드 리뷰
- 최종 병합
- 모니터링
충돌 해결 체크리스트
- 충돌 마커 완전히 제거
- 양쪽 변경사항 모두 고려
- 컴파일 확인
- 관련 테스트 실행
- git diff로 최종 확인
키워드
Git, Merge Conflict, 머지 충돌, 리팩토링, 대규모 병합, 브랜치 전략, Rebase, 협업, 실전 사례, 충돌 해결, 코드 리뷰