Git 완벽 가이드: 내부 동작 원리부터 브랜치 전략, 실무 워크플로우까지
Git 완벽 가이드: 내부 동작 원리부터 브랜치 전략, 실무 워크플로우까지
“Git 써보셨죠?”라는 질문은 면접에서 당연하게 나온다. 하지만 add → commit → push만 알면 “merge와 rebase의 차이”, “충돌은 어떻게 해결하세요?”, “Git Flow를 설명해주세요” 같은 후속 질문에 막힌다. 이 글은 Git의 내부 구조부터 실무 브랜치 전략까지 — 면접에서 자신 있게 답하기 위한 모든 것을 다룬다.
1. Git의 기본 구조
1.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
28
29
30
31
32
33
34
35
36
37
38
39
┌────────────────────────────────────────────────────────────────────────┐
│ Git의 세 가지 영역 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Working Directory Staging Area (Index) Repository │
│ (작업 디렉토리) (스테이징 영역) (저장소, .git) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 파일 수정 │ ──→ │ 커밋할 파일 │ ──→ │ 커밋 히스토리│ │
│ │ 코드 작성 │ add │ 목록 관리 │ commit │ 스냅샷 저장 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ■ Working Directory │
│ → 실제 파일을 편집하는 공간 │
│ → .git 폴더를 제외한 프로젝트 전체 │
│ │
│ ■ Staging Area (Index) │
│ → 다음 커밋에 포함할 파일의 스냅샷을 준비하는 공간 │
│ → git add로 등록 │
│ → .git/index 파일에 저장 │
│ → 왜 필요한가? → 변경사항 중 일부만 선별하여 커밋 가능 │
│ │
│ ■ Repository (.git 폴더) │
│ → 커밋된 스냅샷이 영구 저장되는 곳 │
│ → git commit으로 Staging → Repository 이동 │
│ → 모든 커밋 히스토리, 브랜치, 태그 정보 포함 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 파일의 상태 전이 │ │
│ │ │ │
│ │ Untracked ──(add)──→ Staged ──(commit)──→ Committed │ │
│ │ ↑ ↑ │ │ │
│ │ │ │ │ │ │
│ │ 새 파일 생성 수정 후 add 파일 수정 │ │
│ │ ↓ │ │
│ │ Modified │ │
│ │ ──(add)──→ Staged │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
1.2 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
┌────────────────────────────────────────────────────────────────────────┐
│ Git의 네 가지 객체 — 모든 것은 SHA-1 해시로 식별 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ■ Blob (Binary Large Object) │
│ → 파일의 내용 (파일명은 저장 안 함) │
│ → 같은 내용이면 같은 해시 → 중복 저장 없음 │
│ │
│ ■ Tree │
│ → 디렉토리 구조 (파일명 + Blob 참조) │
│ → 하위 디렉토리는 하위 Tree로 참조 │
│ │
│ ■ Commit │
│ → Tree(루트 디렉토리의 스냅샷) + 메타데이터 │
│ → 작성자, 커밋 메시지, 부모 커밋 참조 │
│ → 부모 커밋을 가리키므로 → 연결 리스트처럼 히스토리 형성 │
│ │
│ ■ Tag │
│ → 특정 커밋에 붙이는 이름표 (v1.0.0 등) │
│ │
│ 구조 예시: │
│ │
│ Commit (a1b2c3) │
│ ├── tree: d4e5f6 │
│ ├── parent: 789abc │
│ ├── author: 김철수 <kim@test.com> │
│ └── message: "로그인 기능 추가" │
│ │ │
│ ▼ │
│ Tree (d4e5f6) │
│ ├── blob a1a1a1 README.md │
│ ├── blob b2b2b2 pom.xml │
│ └── tree c3c3c3 src/ │
│ ├── blob d4d4d4 Application.java │
│ └── blob e5e5e5 UserService.java │
│ │
│ 핵심: Git은 "변경사항(diff)"을 저장하는 것이 아니라 │
│ 매 커밋마다 "전체 스냅샷"을 저장한다. │
│ (같은 파일은 같은 Blob을 참조하므로 공간 낭비 없음) │
└────────────────────────────────────────────────────────────────────────┘
1.3 브랜치의 실체
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
┌────────────────────────────────────────────────────────────────────────┐
│ 브랜치 = 커밋을 가리키는 포인터 (40바이트 파일 하나) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ .git/refs/heads/main → a1b2c3d4... (커밋 해시) │
│ .git/refs/heads/feature → e5f6a7b8... (커밋 해시) │
│ .git/HEAD → ref: refs/heads/main (현재 브랜치) │
│ │
│ main feature │
│ ↓ ↓ │
│ [C4] [C5] │
│ │ │ │
│ └────┬───────┘ │
│ │ │
│ [C3] │
│ │ │
│ [C2] │
│ │ │
│ [C1] │
│ │
│ 브랜치 생성 = 포인터 하나 만드는 것 → 거의 비용 없음 │
│ (SVN 같은 중앙 집중 VCS는 디렉토리 전체 복사 → 비용 큼) │
│ │
│ HEAD = "현재 내가 어디에 있는가"를 가리키는 포인터 │
│ → checkout/switch로 HEAD가 가리키는 브랜치를 변경 │
│ │
│ git branch feature → 새 포인터 생성 (40바이트) │
│ git switch feature → HEAD가 feature를 가리킴 │
│ git commit → feature 포인터가 새 커밋으로 이동 │
└────────────────────────────────────────────────────────────────────────┘
2. Merge vs Rebase (면접 단골)
2.1 Merge — 두 브랜치를 합치기
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
┌────────────────────────────────────────────────────────────────────────┐
│ git merge (병합) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ■ Fast-Forward Merge (빨리 감기) │
│ main에서 feature로 분기 후 main에 새 커밋이 없는 경우 │
│ │
│ Before: │
│ main → [C1]─[C2] │
│ └─[C3]─[C4] ← feature │
│ │
│ git switch main && git merge feature │
│ │
│ After: │
│ main → [C1]─[C2]─[C3]─[C4] ← main, feature │
│ │
│ → merge 커밋 없이 main 포인터만 앞으로 이동 │
│ → 히스토리가 일직선 │
│ │
│ ■ 3-Way Merge (삼방향 병합) │
│ main과 feature 양쪽에 새 커밋이 있는 경우 │
│ │
│ Before: │
│ main → [C1]─[C2]─[C5] ← main │
│ └─[C3]─[C4] ← feature │
│ │
│ git switch main && git merge feature │
│ │
│ After: │
│ main → [C1]─[C2]─[C5]─[M] ← main (merge commit) │
│ └─[C3]─[C4]─┘ │
│ │
│ → 두 브랜치의 공통 조상(C2) + main(C5) + feature(C4) 비교 │
│ → Merge Commit(M) 생성 (부모가 2개: C5, C4) │
│ → 히스토리에 합류 지점이 보임 │
└────────────────────────────────────────────────────────────────────────┘
2.2 Rebase — 히스토리를 깔끔하게
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
┌────────────────────────────────────────────────────────────────────────┐
│ git rebase (리베이스) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Before: │
│ main → [C1]─[C2]─[C5] ← main │
│ └─[C3]─[C4] ← feature │
│ │
│ git switch feature && git rebase main │
│ │
│ "feature의 커밋(C3, C4)을 main의 최신(C5) 뒤에 다시 쌓아라" │
│ │
│ After: │
│ main → [C1]─[C2]─[C5] ← main │
│ └─[C3']─[C4'] ← feature │
│ │
│ → C3, C4의 변경사항을 C5 위에 새로 적용 (C3', C4') │
│ → 원래 C3, C4는 더 이상 참조되지 않음 (나중에 GC) │
│ → 히스토리가 일직선 (merge 커밋 없음) │
│ │
│ 이후 main에서 fast-forward merge: │
│ git switch main && git merge feature │
│ main → [C1]─[C2]─[C5]─[C3']─[C4'] ← main, feature │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ⚠️ Rebase의 황금 규칙: │ │
│ │ "이미 push한 커밋은 rebase하지 마라" │ │
│ │ │ │
│ │ 이유: │ │
│ │ rebase는 기존 커밋을 삭제하고 새 커밋을 만듦 │ │
│ │ → 다른 팀원이 원래 커밋(C3)을 기반으로 작업하고 있으면 │ │
│ │ → C3가 사라졌으므로 히스토리가 꼬임 │ │
│ │ → force push 필요 → 다른 사람의 작업이 날아갈 수 있음 │ │
│ │ │ │
│ │ 안전한 사용: │ │
│ │ 로컬에서만 작업한 feature 브랜치를 push 전에 rebase │ │
│ │ → 깔끔한 히스토리로 PR 제출 가능 │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
2.3 Merge vs Rebase 비교
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
┌────────────────────────────────────────────────────────────────────────┐
│ Merge vs Rebase 비교 │
├────────────────┬──────────────────────┬────────────────────────────────┤
│ │ Merge │ Rebase │
├────────────────┼──────────────────────┼────────────────────────────────┤
│ 히스토리 │ 분기/합류가 보임 │ 일직선 (깔끔) │
├────────────────┼──────────────────────┼────────────────────────────────┤
│ Merge 커밋 │ 생성됨 (부모 2개) │ 없음 │
├────────────────┼──────────────────────┼────────────────────────────────┤
│ 원본 커밋 보존 │ ✅ 원본 그대로 │ ✗ 새 커밋으로 재생성 │
├────────────────┼──────────────────────┼────────────────────────────────┤
│ 충돌 해결 │ 1번 (merge 시점) │ 커밋마다 (여러 번 가능) │
├────────────────┼──────────────────────┼────────────────────────────────┤
│ 안전성 │ ✅ 안전 (비파괴적) │ ⚠️ push된 커밋은 위험 │
├────────────────┼──────────────────────┼────────────────────────────────┤
│ 사용 시기 │ 공유 브랜치 합칠 때 │ 로컬 feature 정리 후 PR │
│ │ PR merge │ main 변경사항 가져올 때 │
├────────────────┼──────────────────────┼────────────────────────────────┤
│ 실무 사용 │ PR merge에 주로 사용 │ feature에서 main 최신 반영 │
└────────────────┴──────────────────────┴────────────────────────────────┘
실무에서 일반적인 패턴:
① feature 브랜치에서 작업
② push 전에 git rebase main (최신 main 반영, 히스토리 정리)
③ push 후 PR 생성
④ PR은 merge (Squash Merge 또는 Merge Commit)로 합침
3. 충돌 해결 (Conflict Resolution)
3.1 충돌이 발생하는 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌────────────────────────────────────────────────────────────────────────┐
│ 충돌 = 같은 파일의 같은 부분을 다르게 수정했을 때 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ main: UserService.java 10번째 줄을 "findById" → "getById"로 수정 │
│ feature: UserService.java 10번째 줄을 "findById" → "fetchById"로 수정│
│ │
│ → Git이 어느 쪽을 선택해야 하는지 모름 → 충돌! │
│ │
│ 충돌이 발생하지 않는 경우: │
│ • 서로 다른 파일을 수정 │
│ • 같은 파일이지만 다른 부분(줄)을 수정 │
│ → Git이 자동으로 합침 (Auto-merge) │
└────────────────────────────────────────────────────────────────────────┘
3.2 충돌 해결 과정
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
┌────────────────────────────────────────────────────────────────────────┐
│ 충돌 해결 실전 흐름 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ $ git merge feature │
│ CONFLICT (content): Merge conflict in UserService.java │
│ Automatic merge failed; fix conflicts and then commit the result. │
│ │
│ ■ 충돌 파일 내용: │
│ ┌──────────────────────────────────────────────┐ │
│ │ public User findUser(Long id) { │ │
│ │ <<<<<<< HEAD │ ← 현재 브랜치(main)│
│ │ return userRepository.getById(id); │ │
│ │ ======= │ ← 구분선 │
│ │ return userRepository.fetchById(id); │ │
│ │ >>>>>>> feature │ ← 합치려는 브랜치 │
│ │ } │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ■ 해결 방법: 마커를 제거하고 원하는 코드만 남기기 │
│ ┌──────────────────────────────────────────────┐ │
│ │ public User findUser(Long id) { │ │
│ │ return userRepository.fetchById(id); │ ← 선택/수정 │
│ │ } │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ■ 해결 후: │
│ $ git add UserService.java # 충돌 해결 완료 표시 │
│ $ git commit # merge commit 생성 (또는 자동 메시지)│
│ │
│ ■ 충돌 해결 도구: │
│ • IDE 내장 (IntelliJ: Accept Yours / Accept Theirs / Merge) │
│ • git mergetool (설정된 외부 도구 실행) │
│ • VS Code의 충돌 마커 위 버튼 (Accept Current/Incoming/Both) │
└────────────────────────────────────────────────────────────────────────┘
4. Reset vs Revert (면접 빈출)
4.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
28
29
30
31
32
33
34
35
36
┌────────────────────────────────────────────────────────────────────────┐
│ Reset vs Revert │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ■ git reset — 히스토리를 되돌림 (커밋을 제거) │
│ │
│ Before: [C1]─[C2]─[C3]─[C4] ← HEAD │
│ git reset --soft C2 │
│ After: [C1]─[C2] ← HEAD (C3, C4는 히스토리에서 사라짐) │
│ │
│ --soft: 커밋만 취소, 변경사항은 Staging에 남음 │
│ --mixed: 커밋 + Staging 취소, 변경사항은 Working에 남음 (기본값) │
│ --hard: 모든 것 삭제 (변경사항도 사라짐) ← ⚠️ 위험 │
│ │
│ ⚠️ 이미 push한 커밋을 reset하면 force push 필요 │
│ → 다른 팀원의 작업이 날아갈 수 있음 │
│ → 공유 브랜치에서는 사용 금지 │
│ │
│ ■ git revert — 되돌리는 새 커밋을 생성 │
│ │
│ Before: [C1]─[C2]─[C3]─[C4] ← HEAD │
│ git revert C3 │
│ After: [C1]─[C2]─[C3]─[C4]─[C3'] ← HEAD │
│ (C3의 변경을 취소하는 커밋) │
│ │
│ → 히스토리가 보존됨 (C3, C4가 그대로 남아있음) │
│ → 새로운 커밋(C3')이 C3의 변경사항을 역으로 적용 │
│ → 이미 push한 커밋을 되돌릴 때 안전 │
│ → 공유 브랜치에서 사용 가능 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 선택 기준: │ │
│ │ • 아직 push 안 한 로컬 커밋 → reset (자유롭게) │ │
│ │ • 이미 push한 공유 커밋 → revert (안전하게) │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
5. 실무에서 자주 쓰는 Git 명령어
5.1 Stash — 작업 중 임시 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌────────────────────────────────────────────────────────────────────────┐
│ git stash — "작업 중인데 잠깐 브랜치 바꿔야 할 때" │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 시나리오: │
│ feature 브랜치에서 코딩 중 → 긴급 버그 수정 요청 │
│ → commit하기엔 아직 미완성 → stash로 임시 저장! │
│ │
│ $ git stash # 변경사항 임시 저장 │
│ $ git switch main # main으로 이동 │
│ $ git switch -c hotfix/login-bug # 긴급 수정 브랜치 생성 │
│ # ... 버그 수정 + commit + push ... │
│ $ git switch feature # 원래 브랜치로 복귀 │
│ $ git stash pop # 임시 저장한 변경사항 복원 │
│ │
│ 주요 명령어: │
│ git stash # 변경사항 저장 (스택에 push) │
│ git stash list # 저장 목록 확인 │
│ git stash pop # 꺼내고 삭제 │
│ git stash apply # 꺼내지만 삭제하지 않음 (재사용 가능) │
│ git stash drop stash@{0} # 특정 stash 삭제 │
└────────────────────────────────────────────────────────────────────────┘
5.2 Cherry-pick — 특정 커밋만 가져오기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌────────────────────────────────────────────────────────────────────────┐
│ git cherry-pick — 다른 브랜치의 특정 커밋 하나만 가져오기 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Before: │
│ main → [C1]─[C2]─[C5] ← main │
│ feature → [C1]─[C2]─[C3]─[C4] ← feature │
│ │
│ C3의 변경사항만 main에 적용하고 싶다면: │
│ $ git switch main │
│ $ git cherry-pick C3의해시 │
│ │
│ After: │
│ main → [C1]─[C2]─[C5]─[C3'] ← main │
│ feature → [C1]─[C2]─[C3]─[C4] ← feature │
│ │
│ 사용 사례: │
│ • 핫픽스를 release 브랜치에서 main으로 가져올 때 │
│ • feature 브랜치의 일부 커밋만 먼저 배포해야 할 때 │
│ • 잘못된 브랜치에 커밋한 것을 올바른 브랜치로 옮길 때 │
└────────────────────────────────────────────────────────────────────────┘
5.3 기타 실무 필수 명령어
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
┌────────────────────────────────────────────────────────────────────────┐
│ 자주 쓰는 Git 명령어 모음 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ■ 히스토리 확인 │
│ git log --oneline --graph --all # 브랜치 그래프 포함 간결 로그 │
│ git log --author="김철수" # 특정 작성자의 커밋만 │
│ git log -p UserService.java # 특정 파일의 변경 이력 │
│ git blame UserService.java # 각 줄을 누가 마지막으로 수정했는지│
│ │
│ ■ 변경사항 확인 │
│ git diff # Working vs Staging 차이 │
│ git diff --staged # Staging vs 마지막 커밋 차이 │
│ git diff main..feature # 두 브랜치 간 차이 │
│ │
│ ■ 실수 되돌리기 │
│ git restore 파일명 # Working의 변경 취소 (최신 커밋으로) │
│ git restore --staged 파일명 # Staging 취소 (add 취소) │
│ git commit --amend # 마지막 커밋 메시지/내용 수정 │
│ │
│ ■ 태그 │
│ git tag v1.0.0 # Lightweight 태그 │
│ git tag -a v1.0.0 -m "설명" # Annotated 태그 (권장) │
│ git push origin v1.0.0 # 태그 원격에 push │
│ │
│ ■ 원격 관련 │
│ git remote -v # 원격 저장소 확인 │
│ git fetch # 원격 변경사항 가져오기 (merge 안 함)│
│ git pull # fetch + merge (또는 rebase) │
│ git pull --rebase # fetch + rebase (깔끔한 히스토리) │
└────────────────────────────────────────────────────────────────────────┘
6. 브랜치 전략
6.1 Git Flow
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
┌────────────────────────────────────────────────────────────────────────┐
│ Git Flow — 가장 체계적인 브랜치 전략 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ main ────●──────────────────────●──────────────●───── (배포 버전) │
│ │ ↑ ↑ │
│ │ release/1.0 release/1.1 │
│ │ merge merge │
│ develop ─●──●──●──●────●──────●──●──●──●──────●───── (개발 통합) │
│ │ │ ↑ │ │ ↑ │
│ │ │ merge │ │ merge │
│ │ │ │ │ │ │ │
│ feature/ ──●──●──┘ │ │ ──●──●──┘ │
│ login (기능개발) │ │ (기능개발) │
│ │ │ │
│ feature/ ──●──●──┘ │
│ signup (기능개발) │
│ │
│ hotfix ─────────────────────────────●──── (긴급수정) │
│ ↓ ↓ │
│ main develop │
│ │
│ 브랜치 역할: │
│ ┌────────────┬────────────────────────────────────────────────┐ │
│ │ main │ 배포된 안정 버전. 태그(v1.0.0)로 버전 표시 │ │
│ │ develop │ 다음 릴리스를 위한 개발 통합 브랜치 │ │
│ │ feature/* │ 기능 개발. develop에서 분기 → develop에 merge │ │
│ │ release/* │ 릴리스 준비 (QA, 버그 수정). develop→main │ │
│ │ hotfix/* │ 긴급 버그 수정. main에서 분기 → main+develop │ │
│ └────────────┴────────────────────────────────────────────────┘ │
│ │
│ 장점: 체계적, 버전 관리 명확, 대규모 팀에 적합 │
│ 단점: 브랜치가 많아 복잡, 배포 주기가 긴 프로젝트에 적합 │
└────────────────────────────────────────────────────────────────────────┘
6.2 GitHub Flow
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
┌────────────────────────────────────────────────────────────────────────┐
│ GitHub Flow — 단순하고 빠른 전략 (스타트업, 웹 서비스에 적합) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ main ──●──────────────●──────────────●──────────── (항상 배포 가능) │
│ │ ↑ ↑ │
│ │ PR merge PR merge │
│ │ │ │ │
│ feature/login ──●──●──┘ │ │
│ │ │
│ feature/signup ──────────●──●──●─────┘ │
│ │
│ 규칙: │
│ ① main은 항상 배포 가능한 상태 │
│ ② 기능 개발 = main에서 feature 브랜치 생성 │
│ ③ feature에서 작업 후 PR(Pull Request) 생성 │
│ ④ 코드 리뷰 + CI 통과 후 main에 merge │
│ ⑤ main에 merge되면 즉시 배포 (CD) │
│ │
│ 장점: 단순, 빠른 배포 주기, CI/CD와 잘 어울림 │
│ 단점: 릴리스 버전 관리가 어려움, 큰 기능은 feature flag 필요 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Git Flow vs GitHub Flow 선택 기준 │ │
│ │ │ │
│ │ • 릴리스 주기가 길고 버전이 명확 → Git Flow │ │
│ │ (모바일 앱, 패키지 소프트웨어) │ │
│ │ │ │
│ │ • 수시로 배포, 빠른 반복 → GitHub Flow │ │
│ │ (웹 서비스, SaaS, 스타트업) │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
7. PR(Pull Request)와 코드 리뷰
7.1 PR Merge 전략
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
┌────────────────────────────────────────────────────────────────────────┐
│ PR Merge 방식 3가지 (GitHub/GitLab) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ■ Merge Commit (기본) │
│ feature의 모든 커밋 + merge 커밋 생성 │
│ main: [C1]─[C2]─[C5]─[M] ← merge commit │
│ └─[C3]─[C4]──┘ │
│ → 히스토리에 분기 흔적이 남음 │
│ → 상세한 히스토리 보존 │
│ │
│ ■ Squash and Merge (가장 많이 사용) │
│ feature의 모든 커밋을 하나로 합쳐서 merge │
│ main: [C1]─[C2]─[C5]─[S] ← squashed commit │
│ S = C3 + C4의 변경사항을 합친 하나의 커밋 │
│ → main 히스토리가 깔끔 (feature당 1커밋) │
│ → feature에서의 WIP, 오타 수정 커밋이 안 보임 │
│ → 대부분의 팀에서 선호 │
│ │
│ ■ Rebase and Merge │
│ feature의 커밋을 main 위에 rebase 후 fast-forward │
│ main: [C1]─[C2]─[C5]─[C3']─[C4'] │
│ → 일직선 히스토리, merge 커밋 없음 │
│ → 각 커밋이 그대로 보존 (Squash와 차이) │
└────────────────────────────────────────────────────────────────────────┘
7.2 좋은 커밋 메시지
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
┌────────────────────────────────────────────────────────────────────────┐
│ Conventional Commits │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 형식: <type>: <subject> │
│ │
│ type: │
│ feat: 새 기능 추가 │
│ fix: 버그 수정 │
│ refactor: 리팩토링 (기능 변화 없이 코드 개선) │
│ docs: 문서 변경 │
│ test: 테스트 추가/수정 │
│ chore: 빌드, 설정 파일 등 기타 변경 │
│ style: 코드 포맷팅, 세미콜론 누락 등 (로직 변화 없음) │
│ │
│ 좋은 예: │
│ feat: 소셜 로그인 기능 추가 (카카오, 네이버) │
│ fix: 주문 금액 계산 시 할인 미적용 오류 수정 │
│ refactor: UserService 인증 로직 AuthService로 분리 │
│ │
│ 나쁜 예: │
│ 수정함 │
│ update │
│ asdf │
│ wip │
└────────────────────────────────────────────────────────────────────────┘
8. .gitignore
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
┌────────────────────────────────────────────────────────────────────────┐
│ .gitignore — Git 추적에서 제외할 파일/폴더 지정 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ # Java / Spring Boot 프로젝트 기본 .gitignore │
│ │
│ # 빌드 결과물 │
│ build/ │
│ target/ │
│ *.class │
│ *.jar │
│ │
│ # IDE 설정 │
│ .idea/ │
│ *.iml │
│ .vscode/ │
│ .settings/ │
│ .project │
│ │
│ # 환경 설정 / 민감 정보 ← 가장 중요! │
│ .env │
│ application-local.yml │
│ **/src/main/resources/application-secret.yml │
│ │
│ # OS 파일 │
│ .DS_Store │
│ Thumbs.db │
│ │
│ # 로그 │
│ *.log │
│ logs/ │
│ │
│ ⚠️ 이미 추적 중인 파일을 나중에 .gitignore에 추가하면? │
│ → 추적이 멈추지 않음! 캐시를 지워야 함: │
│ $ git rm --cached application-secret.yml │
│ $ git commit -m "chore: 민감 설정 파일 추적 제거" │
└────────────────────────────────────────────────────────────────────────┘
9. 면접 질문 & 답변
Q1. Merge와 Rebase의 차이를 설명해주세요. 언제 각각 사용하나요?
Merge는 두 브랜치의 변경사항을 합치면서 merge 커밋을 생성합니다. 원본 커밋이 보존되고 히스토리에 분기/합류 지점이 남습니다. 비파괴적이어서 안전하며, 이미 push된 공유 브랜치를 합칠 때 사용합니다.
Rebase는 현재 브랜치의 커밋을 대상 브랜치의 최신 커밋 뒤에 다시 쌓습니다. 히스토리가 일직선이 되어 깔끔하지만, 기존 커밋이 새 해시로 재생성되므로 이미 push한 커밋에 사용하면 안 됩니다.
실무에서는 feature 브랜치에서 main의 최신 변경사항을 가져올 때 git rebase main으로 히스토리를 정리하고, PR merge 시에는 Squash and Merge로 main 히스토리를 깔끔하게 유지합니다.
Q2. 충돌(Conflict)은 왜 발생하고, 어떻게 해결하나요?
같은 파일의 같은 부분을 두 브랜치에서 다르게 수정했을 때 Git이 어느 쪽을 선택할지 모르면 충돌이 발생합니다. 서로 다른 파일이나 같은 파일의 다른 부분을 수정한 경우에는 Git이 자동으로 합칩니다.
충돌이 발생하면 Git이 충돌 마커(<<<<<<<, =======, >>>>>>>)를 파일에 삽입합니다. 개발자가 마커를 제거하고 원하는 코드로 수정한 뒤 git add로 해결 완료를 표시하고 커밋합니다. IntelliJ 같은 IDE에서는 Accept Yours/Accept Theirs/Merge 버튼으로 시각적으로 해결할 수 있습니다.
충돌을 줄이려면 feature 브랜치를 오래 방치하지 않고 main의 변경사항을 자주 반영하는 것이 좋습니다.
Q3. git reset과 git revert의 차이는? 어떤 상황에서 각각 사용하나요?
reset은 커밋을 히스토리에서 제거합니다. --soft는 변경사항을 Staging에 남기고, --mixed(기본)는 Working에 남기고, --hard는 변경사항까지 모두 삭제합니다. 히스토리를 다시 쓰므로 push되지 않은 로컬 커밋을 되돌릴 때 사용합니다.
revert는 특정 커밋의 변경사항을 역으로 적용하는 새 커밋을 생성합니다. 원본 커밋은 히스토리에 그대로 남아있으므로 안전합니다. 이미 push되어 다른 팀원과 공유된 커밋을 되돌릴 때 사용합니다.
핵심은 push 여부입니다. 로컬에서만 작업한 커밋은 reset, 이미 push한 커밋은 revert를 사용합니다.
Q4. Git Flow와 GitHub Flow를 설명해주세요.
Git Flow는 main, develop, feature, release, hotfix 5가지 브랜치를 사용하는 체계적인 전략입니다. develop에서 feature를 분기하여 기능을 개발하고, release 브랜치에서 QA를 거쳐 main에 배포합니다. 릴리스 주기가 길고 버전 관리가 중요한 프로젝트(모바일 앱, 패키지 소프트웨어)에 적합합니다.
GitHub Flow는 main과 feature 브랜치만 사용하는 단순한 전략입니다. main은 항상 배포 가능한 상태를 유지하고, feature 브랜치에서 작업 후 PR → 코드 리뷰 → main merge → 즉시 배포합니다. 수시 배포가 가능한 웹 서비스와 CI/CD 환경에 적합합니다.
실무에서는 GitHub Flow를 기반으로 필요에 따라 develop 브랜치를 추가하는 절충안을 많이 사용합니다.
Q5. Squash and Merge는 무엇이고, 왜 많이 사용하나요?
Squash and Merge는 feature 브랜치의 모든 커밋을 하나의 커밋으로 합쳐서 main에 merge하는 방식입니다. feature에서 “WIP”, “오타 수정”, “리뷰 반영” 같은 중간 커밋들이 main 히스토리에 나타나지 않고, 하나의 의미 있는 커밋만 남습니다.
main의 히스토리가 feature 단위로 깔끔하게 유지되어 git log로 어떤 기능이 언제 추가되었는지 파악하기 쉽습니다. 대부분의 팀에서 PR merge 시 기본 전략으로 사용합니다.
Q6. git stash는 언제 사용하나요?
작업 중인 변경사항이 있는데 커밋하기엔 미완성이고, 브랜치를 바꿔야 할 때 사용합니다. 대표적으로 feature 개발 중 긴급 버그 수정 요청이 올 때, 현재 변경사항을 stash로 임시 저장하고 hotfix 브랜치로 이동합니다.
git stash로 저장하고, git stash pop으로 복원합니다. 스택 구조라 여러 번 stash할 수 있고, git stash list로 목록을 확인합니다.
Q7. .gitignore에 추가했는데도 파일이 계속 추적되는 이유는?
.gitignore는 아직 추적되지 않은(untracked) 파일에만 적용됩니다. 이미 git add로 추적 중인 파일은 .gitignore에 추가해도 계속 추적됩니다.
해결하려면 git rm --cached 파일명으로 Git 인덱스에서 제거(실제 파일은 삭제되지 않음)하고 커밋해야 합니다. 이후부터 .gitignore 규칙이 적용되어 추적에서 제외됩니다.
민감한 정보(비밀번호, API 키)가 포함된 파일은 처음부터 .gitignore에 등록하는 것이 중요합니다. 한 번이라도 커밋된 파일은 히스토리에 남아 git filter-branch나 BFG Cleaner로 별도 정리해야 합니다.
정리
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
┌────────────────────────────────────────────────────────────────────────┐
│ 핵심 요약 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Git의 3영역: Working → Staging(add) → Repository(commit) │
│ 브랜치 = 커밋을 가리키는 40바이트 포인터 │
│ │
│ 2. Merge vs Rebase │
│ Merge: 안전, merge 커밋 생성, 공유 브랜치에 사용 │
│ Rebase: 깔끔한 히스토리, push 전 로컬 브랜치에만 사용 │
│ │
│ 3. Reset vs Revert │
│ Reset: 히스토리 제거 → 로컬 커밋 되돌리기 │
│ Revert: 새 커밋 생성 → push된 커밋 안전하게 되돌리기 │
│ │
│ 4. 브랜치 전략 │
│ Git Flow: 체계적, 릴리스 주기 긴 프로젝트 │
│ GitHub Flow: 단순, 수시 배포, CI/CD 환경 │
│ │
│ 5. PR Merge = Squash and Merge가 실무 표준 │
│ feature당 1커밋으로 main 히스토리 깔끔 │
│ │
│ 6. 커밋 메시지: feat/fix/refactor + 의미 있는 설명 │
│ .gitignore: 빌드 결과물, IDE 설정, 민감 정보 제외 │
└────────────────────────────────────────────────────────────────────────┘