본문으로 건너뛰기
Previous
Next
Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기

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

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

이 글의 핵심

3개월간 진행된 대규모 리팩토링 브랜치를 main에 병합하면서 발생한 복잡한 머지 충돌을 해결한 실전 사례. 충돌 최소화 전략부터 단계적 병합, 테스트까지 전 과정을 다룹니다.

들어가며

3개월간 진행된 대규모 리팩토링 브랜치를 main에 병합하려는데, 수백 개 파일에서 충돌이 발생했습니다. 이 글에서는 복잡한 머지 충돌을 체계적으로 해결한 실전 사례를 공유합니다. 일상에 빗대면, 오래된 도면과 최신 현장 사진을 한 번에 겹쳐 붙이려는 작업과 비슷합니다. 한 장에 합치려면 어느 층·어느 구역부터 맞출지 나누지 않으면 금방 엉킵니다.

이 글을 읽으면

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

실전 경험에서 배운 교훈

이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.

가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.

이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.

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"
  1. .gitattributes로 머지 전략 설정:
    # .gitattributes
    *.lock merge=ours  # 락 파일은 우리 것 사용
    *.generated.* merge=theirs  # 생성 파일은 최신 것 사용
  2. 충돌 마커 검색:
    # 충돌이 남아있는지 확인
    $ 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, 협업, 실전 사례, 충돌 해결, 코드 리뷰

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Git, Merge Conflict, 머지 충돌, 리팩토링, Rebase, 실전 사례, 협업 등으로 검색하시면 이 글이 도움이 됩니다.