Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기

Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기

이 글의 핵심

Git 대규모 리팩토링 브랜치 병합 실전 - 머지 충돌 최소화 및 단계적 해결 전략

들어가며

3개월간 진행된 대규모 리팩토링 브랜치를 main에 병합하려는데, 수백 개 파일에서 충돌이 발생했습니다. 이 글에서는 복잡한 머지 충돌을 체계적으로 해결한 실전 사례를 공유합니다.

일상에 빗대면, 오래된 도면과 최신 현장 사진을 한 번에 겹쳐 붙이려는 작업과 비슷합니다. 한 장에 합치려면 어느 층·어느 구역부터 맞출지 나누지 않으면 금방 엉킵니다.

이 글을 읽으면

  • 대규모 브랜치 병합 전략을 배웁니다
  • 머지 충돌을 최소화하는 방법을 익힙니다
  • 복잡한 충돌을 단계적으로 해결하는 기법을 이해합니다
  • 병합 후 회귀를 방지하는 테스트 전략을 습득합니다

목차

  1. 상황: 3개월 리팩토링 브랜치
  2. 첫 병합 시도와 실패
  3. 전략 수립: 단계적 병합
  4. 1단계: main을 리팩토링 브랜치로 머지
  5. 2단계: 충돌 분류 및 우선순위
  6. 3단계: 자동 해결 가능한 충돌
  7. 4단계: 수동 해결이 필요한 충돌
  8. 5단계: 테스트 및 검증
  9. 6단계: main으로 최종 병합
  10. 교훈: 충돌 최소화 전략
  11. 마무리

1. 상황: 3개월 리팩토링 브랜치

왜 이렇게까지 충돌이 커졌나

배경은 단순합니다. 장기 브랜치 동안 main에는 기능·버그픽스가 계속 들어왔고, 리팩토링 쪽에서는 디렉터리·클래스명까지 크게 움직였습니다. 같은 파일을 양쪽에서 다르게 진화시킨 결과, 병합 시점에 마커가 폭발한 것입니다.

리팩토링 내용

  • 브랜치: refactor/new-architecture
  • 기간: 2025년 12월 ~ 2026년 3월 (3개월)
  • 변경 범위:
    • 디렉토리 구조 변경 (src/app/)
    • 클래스 이름 변경 (UserManagerUserService)
    • 의존성 주입 패턴 도입
    • 테스트 코드 전면 재작성

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. 전략 수립: 단계적 병합

전략

  1. main을 리팩토링 브랜치로 먼저 머지 (역방향)
  2. 충돌을 카테고리별로 분류
  3. 자동 해결 가능한 충돌 먼저 처리
  4. 수동 해결 필요한 충돌은 하나씩
  5. 테스트 통과 확인
  6. 최종적으로 리팩토링 브랜치를 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. 교훈: 충돌 최소화 전략

핵심 교훈

  1. 자주 동기화: 장기 브랜치는 주기적으로 main을 머지
  2. 작은 단위로 분할: 가능하면 리팩토링을 여러 PR로 나누기
  3. 충돌 분류: 자동/수동 해결 구분하여 효율적으로 처리
  4. 테스트 필수: 충돌 해결 후 반드시 테스트

장기 브랜치 관리 전략

# 매주 main을 리팩토링 브랜치로 머지
$ git checkout refactor/new-architecture
$ git merge main
# 충돌 해결 (작은 단위로)

# 또는 rebase (히스토리를 깔끔하게)
$ git rebase main
# 단, 이미 공유된 브랜치면 rebase 주의

충돌 최소화 팁

  1. 파일 이동과 수정 분리:

    # 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"
  2. .gitattributes로 머지 전략 설정:

    # .gitattributes
    *.lock merge=ours  # 락 파일은 우리 것 사용
    *.generated.* merge=theirs  # 생성 파일은 최신 것 사용
  3. 충돌 마커 검색:

    # 충돌이 남아있는지 확인
    $ 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

마무리

대규모 리팩토링 브랜치 병합은 어렵지만, 체계적인 전략으로 해결할 수 있습니다:

  1. 역방향 머지로 main을 안전하게 보호
  2. 충돌 분류로 효율적 처리
  3. 단계적 해결로 실수 최소화
  4. 테스트 필수로 회귀 방지

핵심: 한 번에 모든 충돌을 해결하려 하지 말고, 카테고리별로 나누어 처리하세요.


FAQ

Q1. rebase vs merge 중 뭘 써야 하나요?

장기 브랜치는 merge를 권장합니다. rebase는 히스토리를 다시 쓰므로, 이미 공유된 브랜치에서는 위험합니다.

Q2. 충돌이 너무 많으면 어떻게 하나요?

브랜치를 더 작은 단위로 나누는 것을 고려하세요. 예: 디렉토리 구조 변경 → 이름 변경 → 로직 변경을 각각 별도 PR로.

Q3. main에서 새로 추가된 파일은 어떻게 하나요?

리팩토링 브랜치에서 새 구조에 맞게 수정하여 추가하세요.


관련 글

  • Git 브랜치와 병합 완벽 가이드
  • Git Rebase 마스터하기
  • Git 협업 워크플로우

실전 체크리스트

대규모 병합 체크리스트

  • 백업 브랜치 생성
  • 충돌 개수 파악
  • 충돌 분류 (자동/수동)
  • 자동 해결 가능한 충돌 먼저 처리
  • 수동 충돌 하나씩 해결
  • 컴파일 확인
  • 테스트 실행
  • 코드 리뷰
  • 최종 병합
  • 모니터링

충돌 해결 체크리스트

  • 충돌 마커 완전히 제거
  • 양쪽 변경사항 모두 고려
  • 컴파일 확인
  • 관련 테스트 실행
  • git diff로 최종 확인

키워드

Git, Merge Conflict, 머지 충돌, 리팩토링, 대규모 병합, 브랜치 전략, Rebase, 협업, 실전 사례, 충돌 해결, 코드 리뷰