포스트

[이제와서 시작하는 GitHub 마스터하기 - 심화편 #2] Git 내부 동작 원리: .git 폴더의 비밀

[이제와서 시작하는 GitHub 마스터하기 - 심화편 #2] Git 내부 동작 원리: .git 폴더의 비밀

들어가며

GitHub 마스터하기 심화편 두 번째 시간입니다. 이번에는 Git의 내부 동작 원리를 깊이 있게 탐구해보겠습니다. Git이 어떻게 파일을 저장하고, 커밋을 관리하며, 브랜치를 다루는지 이해하면 Git을 훨씬 더 효과적으로 사용할 수 있습니다. .git 폴더 속 숨겨진 비밀을 파헤쳐봅시다.

1. Git의 핵심 개념: Content-Addressable Storage

Git은 Key-Value 데이터베이스

1
2
3
4
5
6
7
8
9
10
11
# Git의 핵심 원리 시연
echo 'Hello, Git!' | git hash-object --stdin
# 출력: d5b8f77ce1dc1a37b29885026055c8656c3e0ceb

# 동일한 내용은 항상 같은 해시
echo 'Hello, Git!' | git hash-object --stdin
# 출력: d5b8f77ce1dc1a37b29885026055c8656c3e0ceb

# 내용이 조금만 달라도 완전히 다른 해시
echo 'Hello, Git!!' | git hash-object --stdin
# 출력: b7e23ec29af22b0b930771f0e8f8c6c6e8897b3e

SHA-1 해시의 의미

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# git_hash.py - Git 해시 생성 원리
import hashlib
import zlib

def git_hash_object(content, obj_type='blob'):
    """Git이 객체를 해싱하는 방법"""
    # Git 객체 형식: "타입 크기\0내용"
    store = f"{obj_type} {len(content)}\0{content}"
    
    # SHA-1 해시 계산
    sha1 = hashlib.sha1(store.encode()).hexdigest()
    
    # 실제 저장될 압축 데이터
    compressed = zlib.compress(store.encode())
    
    return {
        'hash': sha1,
        'raw_content': store,
        'compressed_size': len(compressed),
        'original_size': len(content)
    }

# 예제
result = git_hash_object("Hello, Git!")
print(f"Hash: {result['hash']}")
print(f"Original size: {result['original_size']} bytes")
print(f"Compressed size: {result['compressed_size']} bytes")

2. .git 디렉토리 구조 심화 분석

디렉토리 구조와 역할

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.git/
├── HEAD              # 현재 체크아웃된 참조
├── config            # 저장소별 설정
├── description       # GitWeb용 설명
├── hooks/            # 클라이언트/서버 훅
├── info/             # 전역 exclude 패턴
├── objects/          # 모든 데이터 저장소
│   ├── 00/          # 해시의 첫 2자리로 분류
│   ├── 01/
│   ├── ...
│   ├── info/        # 추가 객체 정보
│   └── pack/        # 팩 파일 (압축된 객체들)
├── refs/             # 참조 (브랜치, 태그)
│   ├── heads/       # 로컬 브랜치
│   ├── remotes/     # 원격 브랜치
│   └── tags/        # 태그
├── logs/             # 참조 로그 (reflog)
├── index             # 스테이징 영역
└── packed-refs       # 성능을 위한 참조 압축

Git 객체 타입 상세

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 1. Blob (Binary Large Object) - 파일 내용
echo "File content" > test.txt
git add test.txt
git ls-files --stage
# 100644 7a9c...  0	test.txt

# Blob 객체 내용 확인
git cat-file -p 7a9c...
# File content

# 2. Tree - 디렉토리 구조
git write-tree
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904

git cat-file -p 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# 100644 blob 7a9c...    test.txt

# 3. Commit - 스냅샷 + 메타데이터
git commit -m "Initial commit"
git cat-file -p HEAD
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# author Your Name <email> 1234567890 +0000
# committer Your Name <email> 1234567890 +0000
#
# Initial commit

# 4. Tag - 특정 커밋 참조
git tag -a v1.0 -m "Version 1.0"
git cat-file -p v1.0
# object abc123...
# type commit
# tag v1.0
# tagger Your Name <email> 1234567890 +0000
#
# Version 1.0

3. Git의 3단계 아키텍처

Working Directory, Staging Area, Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# git_stages.py - Git의 3단계 시뮬레이션
import os
import hashlib
import json
from datetime import datetime

class MiniGit:
    def __init__(self, repo_path='.minigit'):
        self.repo_path = repo_path
        self.objects_path = os.path.join(repo_path, 'objects')
        self.index_path = os.path.join(repo_path, 'index')
        self.head_path = os.path.join(repo_path, 'HEAD')
        
        # 초기화
        os.makedirs(self.objects_path, exist_ok=True)
        
    def hash_object(self, content, obj_type='blob'):
        """객체 해싱 및 저장"""
        header = f"{obj_type} {len(content)}\0"
        store = header + content
        sha = hashlib.sha1(store.encode()).hexdigest()
        
        # 객체 저장
        obj_dir = os.path.join(self.objects_path, sha[:2])
        os.makedirs(obj_dir, exist_ok=True)
        
        obj_path = os.path.join(obj_dir, sha[2:])
        with open(obj_path, 'wb') as f:
            f.write(store.encode())
            
        return sha
    
    def add(self, filename):
        """파일을 스테이징 영역에 추가"""
        with open(filename, 'r') as f:
            content = f.read()
            
        # Blob 객체 생성
        sha = self.hash_object(content)
        
        # 인덱스 업데이트
        index = self.read_index()
        index[filename] = {
            'sha': sha,
            'mtime': os.path.getmtime(filename),
            'size': os.path.getsize(filename)
        }
        self.write_index(index)
        
        print(f"Added {filename} ({sha[:7]})")
        
    def commit(self, message):
        """커밋 생성"""
        index = self.read_index()
        if not index:
            print("Nothing to commit")
            return
            
        # Tree 객체 생성
        tree_content = ""
        for filename, info in sorted(index.items()):
            tree_content += f"100644 blob {info['sha']}\t{filename}\n"
            
        tree_sha = self.hash_object(tree_content, 'tree')
        
        # Commit 객체 생성
        parent = self.get_head()
        commit_content = f"tree {tree_sha}\n"
        if parent:
            commit_content += f"parent {parent}\n"
        commit_content += f"author MiniGit <mini@git.com> {int(datetime.now().timestamp())} +0000\n"
        commit_content += f"committer MiniGit <mini@git.com> {int(datetime.now().timestamp())} +0000\n"
        commit_content += f"\n{message}\n"
        
        commit_sha = self.hash_object(commit_content, 'commit')
        
        # HEAD 업데이트
        with open(self.head_path, 'w') as f:
            f.write(commit_sha)
            
        # 인덱스 클리어
        self.write_index({})
        
        print(f"Committed {commit_sha[:7]}: {message}")
        
    def read_index(self):
        """인덱스 읽기"""
        if not os.path.exists(self.index_path):
            return {}
        with open(self.index_path, 'r') as f:
            return json.load(f)
            
    def write_index(self, index):
        """인덱스 쓰기"""
        with open(self.index_path, 'w') as f:
            json.dump(index, f)
            
    def get_head(self):
        """현재 HEAD 커밋"""
        if not os.path.exists(self.head_path):
            return None
        with open(self.head_path, 'r') as f:
            return f.read().strip()

# 사용 예제
git = MiniGit()
git.add('file1.txt')
git.add('file2.txt')
git.commit('Initial commit')

실제 Git 명령어로 내부 동작 추적

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 파일 추가 과정 추적
echo "Hello" > hello.txt
strace -e trace=file git add hello.txt 2>&1 | grep -E "(open|write)"

# 2. 인덱스 파일 분석
hexdump -C .git/index | head -20

# 3. 객체 데이터베이스 직접 조작
# Blob 객체 수동 생성
echo -en "blob 6\0Hello\n" | shasum
# 1c59427adc4b205a270d8f810310394962e79a8b

# 객체 저장
mkdir -p .git/objects/1c
echo -en "blob 6\0Hello\n" | zlib-flate -compress > .git/objects/1c/59427adc4b205a270d8f810310394962e79a8b

# 확인
git cat-file -p 1c59427adc4b205a270d8f810310394962e79a8b
# Hello

4. Pack 파일과 델타 압축

Pack 파일 생성과 분석

1
2
3
4
5
6
7
8
9
10
# Pack 파일 수동 생성
git gc --aggressive

# Pack 파일 내용 확인
git verify-pack -v .git/objects/pack/pack-*.idx

# Pack 파일 언팩
mkdir unpacked
cd unpacked
git unpack-objects < ../.git/objects/pack/pack-*.pack

델타 압축 원리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# delta_compression.py - Git 델타 압축 시뮬레이션
import difflib

class DeltaCompression:
    def __init__(self):
        self.objects = {}
        
    def store_object(self, content):
        """객체 저장 (델타 압축 적용)"""
        # 기존 객체들과 비교
        best_base = None
        best_delta = None
        min_size = len(content)
        
        for sha, base_content in self.objects.items():
            delta = self.create_delta(base_content, content)
            if len(delta) < min_size:
                best_base = sha
                best_delta = delta
                min_size = len(delta)
                
        # 델타가 더 효율적이면 델타로 저장
        if best_delta and min_size < len(content) * 0.5:
            return self.store_delta(best_base, best_delta)
        else:
            return self.store_full(content)
            
    def create_delta(self, base, target):
        """델타 생성"""
        # 실제 Git은 더 복잡한 알고리즘 사용
        delta = []
        matcher = difflib.SequenceMatcher(None, base, target)
        
        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == 'equal':
                delta.append(('copy', i1, i2 - i1))
            elif tag == 'insert':
                delta.append(('insert', target[j1:j2]))
            elif tag == 'replace':
                delta.append(('insert', target[j1:j2]))
                
        return delta
        
    def apply_delta(self, base_content, delta):
        """델타 적용하여 원본 복원"""
        result = []
        for cmd in delta:
            if cmd[0] == 'copy':
                _, start, length = cmd
                result.append(base_content[start:start+length])
            elif cmd[0] == 'insert':
                _, data = cmd
                result.append(data)
                
        return ''.join(result)

# 예제
dc = DeltaCompression()
dc.objects['base'] = "Hello, World! This is a test."
content = "Hello, World! This is a test with small changes."
dc.store_object(content)

5. Git References와 Reflog

References 시스템 이해

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 참조 타입들
find .git/refs -type f | head -10

# 2. Symbolic references
cat .git/HEAD
# ref: refs/heads/main

# 3. 참조 수동 생성
echo "abc123..." > .git/refs/heads/experimental

# 4. Packed refs
cat .git/packed-refs

# 5. 참조 업데이트 로그
tail -10 .git/logs/HEAD
tail -10 .git/logs/refs/heads/main

Reflog 심화 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. Reflog 전체 보기
git reflog show --all

# 2. 특정 참조의 reflog
git reflog show main

# 3. Reflog 항목 상세 정보
git reflog show --date=iso

# 4. Reflog 기반 복구
# 실수로 reset한 경우
git reset --hard HEAD~3
# 복구
git reflog
git reset --hard HEAD@{1}

# 5. Reflog 정리
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Reflog 데이터 구조 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# reflog_analyzer.py
import re
from datetime import datetime

class ReflogAnalyzer:
    def __init__(self, reflog_file):
        self.entries = self.parse_reflog(reflog_file)
        
    def parse_reflog(self, filename):
        """Reflog 파일 파싱"""
        entries = []
        
        with open(filename, 'r') as f:
            for line in f:
                match = re.match(
                    r'(\w{40}) (\w{40}) (.+?) (\d+) ([+-]\d{4})\t(.+)',
                    line.strip()
                )
                if match:
                    old_sha, new_sha, author, timestamp, tz, message = match.groups()
                    entries.append({
                        'old_sha': old_sha,
                        'new_sha': new_sha,
                        'author': author,
                        'timestamp': datetime.fromtimestamp(int(timestamp)),
                        'timezone': tz,
                        'message': message
                    })
                    
        return entries
        
    def find_lost_commits(self):
        """잃어버린 커밋 찾기"""
        all_shas = set()
        current_shas = set()
        
        for entry in self.entries:
            all_shas.add(entry['old_sha'])
            all_shas.add(entry['new_sha'])
            
        # 현재 접근 가능한 커밋
        # 실제로는 git rev-list --all 사용
        current_shas = self.get_reachable_commits()
        
        lost = all_shas - current_shas
        return lost
        
    def analyze_patterns(self):
        """작업 패턴 분석"""
        patterns = {
            'commits': 0,
            'resets': 0,
            'checkouts': 0,
            'merges': 0,
            'rebases': 0
        }
        
        for entry in self.entries:
            msg = entry['message']
            if 'commit' in msg:
                patterns['commits'] += 1
            elif 'reset' in msg:
                patterns['resets'] += 1
            elif 'checkout' in msg:
                patterns['checkouts'] += 1
            elif 'merge' in msg:
                patterns['merges'] += 1
            elif 'rebase' in msg:
                patterns['rebases'] += 1
                
        return patterns

# 사용 예제
analyzer = ReflogAnalyzer('.git/logs/HEAD')
patterns = analyzer.analyze_patterns()
print(f"Work patterns: {patterns}")

6. Git Hooks 내부 동작

Hook 실행 메커니즘

1
2
3
4
5
6
# hooks 디렉토리 구조
ls -la .git/hooks/

# Hook 샘플 활성화
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

고급 Hook 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env python3
# .git/hooks/pre-commit
"""고급 pre-commit hook"""

import subprocess
import sys
import re

def run_command(cmd):
    """명령 실행 및 결과 반환"""
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.returncode, result.stdout, result.stderr

def check_file_size():
    """대용량 파일 체크"""
    MAX_SIZE = 100 * 1024 * 1024  # 100MB
    
    rc, stdout, _ = run_command("git diff --cached --name-only")
    if rc != 0:
        return False
        
    for filename in stdout.strip().split('\n'):
        if not filename:
            continue
            
        rc, size, _ = run_command(f"git cat-file -s :0:{filename}")
        if rc == 0 and int(size) > MAX_SIZE:
            print(f"Error: {filename} is too large ({int(size)/1024/1024:.1f}MB)")
            return False
            
    return True

def check_secrets():
    """민감 정보 검사"""
    patterns = [
        r'(?i)api[_-]?key.*=.*["\'][\w]{20,}["\']',
        r'(?i)secret.*=.*["\'][\w]{20,}["\']',
        r'(?i)password.*=.*["\'].+["\']',
        r'-----BEGIN (RSA |EC )?PRIVATE KEY-----',
        r'[0-9a-f]{40}',  # SHA-1 like
    ]
    
    rc, files, _ = run_command("git diff --cached --name-only")
    if rc != 0:
        return True
        
    for filename in files.strip().split('\n'):
        if not filename:
            continue
            
        rc, content, _ = run_command(f"git show :{filename}")
        if rc != 0:
            continue
            
        for pattern in patterns:
            if re.search(pattern, content):
                print(f"Warning: Possible secret in {filename}")
                print("Use 'git commit --no-verify' to bypass")
                return False
                
    return True

def check_commit_message():
    """커밋 메시지 규칙 검사"""
    rc, msg, _ = run_command("git log -1 --pretty=%B")
    if rc != 0:
        return True
        
    # Conventional Commits 형식 체크
    pattern = r'^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+'
    if not re.match(pattern, msg):
        print("Error: Commit message doesn't follow conventional format")
        print("Format: type(scope): description")
        return False
        
    return True

def main():
    """메인 검증 로직"""
    checks = [
        ("Checking file sizes...", check_file_size),
        ("Scanning for secrets...", check_secrets),
    ]
    
    for message, check_func in checks:
        print(message)
        if not check_func():
            sys.exit(1)
            
    print("All checks passed!")
    sys.exit(0)

if __name__ == "__main__":
    main()

7. Git 객체 스토리지 최적화

객체 저장소 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# git_storage_analyzer.py
import os
import zlib
import hashlib
from collections import defaultdict

class GitStorageAnalyzer:
    def __init__(self, git_dir='.git'):
        self.git_dir = git_dir
        self.objects_dir = os.path.join(git_dir, 'objects')
        
    def analyze_objects(self):
        """객체 저장소 분석"""
        stats = defaultdict(lambda: {'count': 0, 'size': 0, 'compressed': 0})
        
        # Loose objects
        for root, dirs, files in os.walk(self.objects_dir):
            # pack 디렉토리 제외
            if 'pack' in root:
                continue
                
            for file in files:
                if len(file) == 38:  # SHA-1 나머지 부분
                    path = os.path.join(root, file)
                    obj_type, size, compressed_size = self.analyze_object(path)
                    
                    stats[obj_type]['count'] += 1
                    stats[obj_type]['size'] += size
                    stats[obj_type]['compressed'] += compressed_size
                    
        return stats
        
    def analyze_object(self, path):
        """개별 객체 분석"""
        with open(path, 'rb') as f:
            compressed = f.read()
            
        # 압축 해제
        decompressed = zlib.decompress(compressed)
        
        # 헤더 파싱
        header_end = decompressed.find(b'\0')
        header = decompressed[:header_end].decode()
        obj_type, size_str = header.split(' ')
        
        return obj_type, int(size_str), len(compressed)
        
    def find_duplicates(self):
        """중복 객체 찾기"""
        content_map = defaultdict(list)
        
        for root, dirs, files in os.walk(self.objects_dir):
            if 'pack' in root:
                continue
                
            for file in files:
                if len(file) == 38:
                    path = os.path.join(root, file)
                    sha = os.path.basename(root) + file
                    
                    with open(path, 'rb') as f:
                        content = f.read()
                        
                    content_hash = hashlib.md5(content).hexdigest()
                    content_map[content_hash].append(sha)
                    
        # 중복 찾기
        duplicates = {k: v for k, v in content_map.items() if len(v) > 1}
        return duplicates
        
    def suggest_gc_settings(self, stats):
        """GC 설정 제안"""
        total_objects = sum(s['count'] for s in stats.values())
        total_size = sum(s['size'] for s in stats.values())
        
        suggestions = []
        
        if total_objects > 10000:
            suggestions.append("git config gc.auto 5000")
            suggestions.append("Consider running 'git gc --aggressive'")
            
        if total_size > 100 * 1024 * 1024:  # 100MB
            suggestions.append("git config pack.packSizeLimit 10m")
            suggestions.append("Consider using 'git repack -a -d'")
            
        return suggestions

# 사용 예제
analyzer = GitStorageAnalyzer()
stats = analyzer.analyze_objects()

print("Object Statistics:")
for obj_type, data in stats.items():
    compression_ratio = (1 - data['compressed'] / data['size']) * 100 if data['size'] > 0 else 0
    print(f"{obj_type}: {data['count']} objects, "
          f"{data['size']/1024:.1f}KB -> {data['compressed']/1024:.1f}KB "
          f"({compression_ratio:.1f}% compression)")

8. Git 네트워크 프로토콜

Git 전송 프로토콜 분석

1
2
3
4
5
6
7
8
# 1. HTTP(S) 프로토콜 디버깅
GIT_TRACE_PACKET=1 GIT_TRACE=1 GIT_CURL_VERBOSE=1 git clone https://github.com/user/repo

# 2. SSH 프로토콜 디버깅
GIT_SSH_COMMAND="ssh -v" git clone git@github.com:user/repo

# 3. Git 프로토콜 분석
GIT_TRACE_PACKET=1 git ls-remote git://github.com/user/repo

Pack 프로토콜 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# git_protocol.py - Git 네트워크 프로토콜 시뮬레이션
import struct
import zlib

class GitProtocol:
    def __init__(self):
        self.capabilities = [
            'multi_ack_detailed',
            'side-band-64k',
            'ofs-delta',
            'agent=git/2.0.0'
        ]
        
    def pkt_line(self, data):
        """Git pkt-line 형식으로 인코딩"""
        if data is None:
            return b'0000'
            
        if isinstance(data, str):
            data = data.encode()
            
        length = len(data) + 4
        return f'{length:04x}'.encode() + data
        
    def parse_pkt_line(self, data):
        """pkt-line 파싱"""
        lines = []
        offset = 0
        
        while offset < len(data):
            length_hex = data[offset:offset+4].decode()
            if length_hex == '0000':
                break
                
            length = int(length_hex, 16)
            line = data[offset+4:offset+length]
            lines.append(line)
            offset += length
            
        return lines
        
    def ref_advertisement(self, refs):
        """참조 광고 생성"""
        lines = []
        
        # 첫 번째 참조는 capabilities 포함
        first_ref = list(refs.items())[0]
        caps = '\0' + ' '.join(self.capabilities)
        lines.append(f"{first_ref[1]} {first_ref[0]}{caps}")
        
        # 나머지 참조들
        for ref, sha in list(refs.items())[1:]:
            lines.append(f"{sha} {ref}")
            
        # pkt-line 형식으로 변환
        result = b''
        for line in lines:
            result += self.pkt_line(line)
            
        result += self.pkt_line(None)  # flush
        return result
        
    def parse_want_list(self, data):
        """클라이언트 want 리스트 파싱"""
        wants = []
        lines = self.parse_pkt_line(data)
        
        for line in lines:
            if line.startswith(b'want '):
                sha = line[5:45].decode()
                wants.append(sha)
                
        return wants
        
    def create_pack(self, objects):
        """Pack 데이터 생성"""
        # Pack 헤더
        signature = b'PACK'
        version = struct.pack('>I', 2)
        count = struct.pack('>I', len(objects))
        
        pack_data = signature + version + count
        
        # 객체들 추가
        for obj in objects:
            # 간단한 구현 (실제는 더 복잡)
            obj_type = 1  # commit
            size = len(obj['data'])
            
            # 타입과 크기 인코딩
            header = (obj_type << 4) | (size & 0x0f)
            size >>= 4
            
            while size:
                header |= 0x80
                pack_data += bytes([header])
                header = size & 0x7f
                size >>= 7
                
            pack_data += bytes([header])
            
            # 압축된 데이터
            pack_data += zlib.compress(obj['data'])
            
        # 체크섬
        # 실제로는 SHA-1 사용
        checksum = b'\x00' * 20
        pack_data += checksum
        
        return pack_data

# 프로토콜 시뮬레이션
protocol = GitProtocol()

# 서버 측 참조 광고
refs = {
    'refs/heads/main': 'abc123' + '0' * 34,
    'refs/heads/develop': 'def456' + '0' * 34,
}
ref_adv = protocol.ref_advertisement(refs)
print(f"Reference advertisement: {len(ref_adv)} bytes")

# 클라이언트 요청
want_request = protocol.pkt_line('want abc1230000000000000000000000000000000000')
wants = protocol.parse_want_list(want_request)
print(f"Client wants: {wants}")

9. Git 성능 최적화 기법

고급 설정과 튜닝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 코어 설정
git config core.preloadindex true  # 인덱스 미리 로드
git config core.fscache true       # 파일시스템 캐시
git config core.commitGraph true   # 커밋 그래프 사용

# 2. Pack 설정
git config pack.threads 0          # CPU 코어 수만큼 스레드
git config pack.windowMemory 1g    # Pack 윈도우 메모리
git config pack.packSizeLimit 2g   # Pack 파일 크기 제한

# 3. GC 설정
git config gc.auto 5000           # 자동 GC 트리거
git config gc.autopacklimit 50    # Pack 파일 수 제한
git config gc.pruneExpire 2.weeks.ago

# 4. 전송 설정
git config http.postBuffer 524288000  # 500MB
git config core.compression 9         # 최대 압축

# 5. Diff 설정
git config diff.algorithm histogram   # 더 나은 diff 알고리즘
git config diff.renames copies       # 복사 감지

성능 벤치마킹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env python3
# git_benchmark.py
import time
import subprocess
import statistics

class GitBenchmark:
    def __init__(self, repo_path):
        self.repo_path = repo_path
        
    def benchmark_operation(self, operation, iterations=5):
        """Git 작업 벤치마킹"""
        times = []
        
        for _ in range(iterations):
            start = time.time()
            subprocess.run(operation, shell=True, cwd=self.repo_path,
                         capture_output=True)
            end = time.time()
            times.append(end - start)
            
        return {
            'mean': statistics.mean(times),
            'median': statistics.median(times),
            'stdev': statistics.stdev(times) if len(times) > 1 else 0,
            'min': min(times),
            'max': max(times)
        }
        
    def run_benchmarks(self):
        """주요 작업 벤치마크"""
        operations = {
            'status': 'git status',
            'log': 'git log --oneline -100',
            'diff': 'git diff HEAD~10',
            'add_all': 'git add -A',
            'commit': 'git commit --amend --no-edit',
            'checkout': 'git checkout HEAD~1 && git checkout -'
        }
        
        results = {}
        for name, cmd in operations.items():
            print(f"Benchmarking {name}...")
            results[name] = self.benchmark_operation(cmd)
            
        return results
        
    def optimize_repo(self):
        """저장소 최적화"""
        optimizations = [
            ('Garbage collection', 'git gc --aggressive'),
            ('Repack', 'git repack -a -d -f --depth=250 --window=250'),
            ('Commit graph', 'git commit-graph write --reachable'),
            ('Prune', 'git prune --expire now')
        ]
        
        for desc, cmd in optimizations:
            print(f"Running {desc}...")
            subprocess.run(cmd, shell=True, cwd=self.repo_path)
            
# 사용 예제
benchmark = GitBenchmark('/path/to/large/repo')
print("Before optimization:")
before = benchmark.run_benchmarks()

benchmark.optimize_repo()

print("\nAfter optimization:")
after = benchmark.run_benchmarks()

# 개선율 계산
for op in before:
    improvement = (before[op]['mean'] - after[op]['mean']) / before[op]['mean'] * 100
    print(f"{op}: {improvement:.1f}% improvement")

10. Git 데이터 복구

손상된 저장소 복구

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 저장소 검증
git fsck --full --no-reflogs

# 2. 손상된 객체 찾기
find .git/objects -type f -empty

# 3. 객체 복구 시도
git hash-object -w path/to/file

# 4. 인덱스 재구성
rm .git/index
git reset

# 5. Reflog에서 복구
git reflog expire --expire=now --all
git fsck --full --no-reflogs | grep commit

고급 복구 스크립트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/env python3
# git_recovery.py
import os
import subprocess
import zlib
import hashlib

class GitRecovery:
    def __init__(self, git_dir='.git'):
        self.git_dir = git_dir
        
    def find_corrupted_objects(self):
        """손상된 객체 찾기"""
        corrupted = []
        
        result = subprocess.run(['git', 'fsck', '--full'], 
                              capture_output=True, text=True)
                              
        for line in result.stderr.split('\n'):
            if 'corrupt' in line or 'missing' in line:
                # SHA 추출
                parts = line.split()
                for part in parts:
                    if len(part) == 40 and all(c in '0123456789abcdef' for c in part):
                        corrupted.append(part)
                        
        return corrupted
        
    def recover_from_pack(self, sha):
        """Pack 파일에서 객체 복구 시도"""
        pack_dir = os.path.join(self.git_dir, 'objects', 'pack')
        
        for pack_file in os.listdir(pack_dir):
            if pack_file.endswith('.idx'):
                pack_base = pack_file[:-4]
                
                # 인덱스에서 객체 찾기
                result = subprocess.run(
                    ['git', 'verify-pack', '-v', os.path.join(pack_dir, pack_file)],
                    capture_output=True, text=True
                )
                
                if sha in result.stdout:
                    print(f"Found {sha} in {pack_base}")
                    # 객체 추출
                    self.extract_from_pack(pack_base, sha)
                    return True
                    
        return False
        
    def recover_from_remote(self, remote='origin'):
        """원격 저장소에서 복구"""
        print(f"Attempting recovery from {remote}...")
        
        # 모든 참조 가져오기
        subprocess.run(['git', 'fetch', remote, '--all'])
        
        # fsck 재실행
        result = subprocess.run(['git', 'fsck', '--full'], 
                              capture_output=True, text=True)
                              
        return 'error' not in result.stderr.lower()
        
    def rebuild_index(self):
        """인덱스 재구성"""
        index_path = os.path.join(self.git_dir, 'index')
        
        # 백업
        if os.path.exists(index_path):
            os.rename(index_path, index_path + '.backup')
            
        # 재구성
        subprocess.run(['git', 'read-tree', 'HEAD'])
        
    def emergency_backup(self, backup_path):
        """긴급 백업"""
        import shutil
        
        print(f"Creating emergency backup at {backup_path}...")
        
        # 중요 파일들 백업
        important_files = [
            'objects',
            'refs',
            'logs',
            'config',
            'HEAD',
            'packed-refs'
        ]
        
        os.makedirs(backup_path, exist_ok=True)
        
        for item in important_files:
            src = os.path.join(self.git_dir, item)
            dst = os.path.join(backup_path, item)
            
            if os.path.exists(src):
                if os.path.isdir(src):
                    shutil.copytree(src, dst, dirs_exist_ok=True)
                else:
                    shutil.copy2(src, dst)
                    
        print("Backup completed")
        
# 사용 예제
recovery = GitRecovery()

# 손상된 객체 찾기
corrupted = recovery.find_corrupted_objects()
print(f"Found {len(corrupted)} corrupted objects")

# 복구 시도
for sha in corrupted:
    if recovery.recover_from_pack(sha):
        print(f"Recovered {sha}")
    else:
        print(f"Failed to recover {sha}")
        
# 원격에서 복구
if corrupted:
    recovery.recover_from_remote()

마무리

Git의 내부 동작 원리를 이해하면 Git을 훨씬 더 효과적으로 사용할 수 있습니다. .git 폴더는 단순한 데이터 저장소가 아니라, 정교하게 설계된 버전 관리 시스템의 핵심입니다.

핵심 포인트:

  • Git은 Content-Addressable 파일시스템
  • 모든 것은 객체(Blob, Tree, Commit, Tag)
  • Pack 파일로 효율적인 저장
  • Reflog로 모든 작업 추적 가능
  • 다양한 복구 옵션 제공

Git의 내부를 이해하면 문제 해결 능력이 크게 향상되고, 더 고급 기능들을 자신 있게 사용할 수 있습니다.

다음 심화편에서는 고급 브랜치 전략과 릴리스 관리에 대해 다루겠습니다.


📚 GitHub 마스터하기 시리즈

🌱 기초편 (입문자)

  1. GitHub 시작하기
  2. Repository 기초
  3. Git 기본 명령어
  4. Branch와 Merge
  5. Fork와 Pull Request

💼 실전편 (중급자)

  1. Issues 활용법
  2. Projects로 프로젝트 관리
  3. Code Review 잘하기
  4. GitHub Discussions
  5. Team 협업 설정
  6. GitHub Pages

🚀 고급편 (전문가)

  1. GitHub Actions 입문
  2. Actions 고급 활용
  3. Webhooks와 API
  4. GitHub Apps 개발
  5. 보안 기능
  6. GitHub Packages
  7. Codespaces
  8. GitHub CLI
  9. 통계와 인사이트

🏆 심화편 (전문가+)

  1. Git Submodules & Subtree
  2. Git 내부 동작 원리 (현재 글)
  3. 고급 브랜치 전략과 릴리스 관리
  4. GitHub GraphQL API
  5. GitHub Copilot 완벽 활용
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.