[이제와서 시작하는 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 마스터하기 시리즈
🌱 기초편 (입문자)
💼 실전편 (중급자)
🚀 고급편 (전문가)
- GitHub Actions 입문
- Actions 고급 활용
- Webhooks와 API
- GitHub Apps 개발
- 보안 기능
- GitHub Packages
- Codespaces
- GitHub CLI
- 통계와 인사이트
🏆 심화편 (전문가+)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.