인프라 팀에서 배포 후 문제가 생겼습니다. 어젯밤 누군가 main 브랜치에 직접 push한 커밋이 있었고, 오늘 오전 배포에서 그 코드가 올라갔습니다. 롤백 요청이 왔는데 팀원들은 "revert해야 하나 reset해야 하나"로 논쟁 중입니다. 릴리즈 태그도 없어서 "정확히 어느 버전까지 돌아가야 하나"도 불분명합니다.
이 모듈은 그런 상황에서 당황하지 않도록, Git을 형상관리 도구로 제대로 쓰는 방법을 다룹니다. 브랜치 전략, 태그 릴리즈, 안전한 롤백 절차를 실무 중심으로 정리합니다.
- 1GitFlow 브랜치 전략(main/develop/feature/release/hotfix)의 흐름을 설명할 수 있다
- 2git tag로 릴리즈 기준점을 만들고 배포 이력을 추적할 수 있다
- 3커밋 메시지에 이슈 번호를 포함해 GitLab에서 이슈-커밋 연결을 활용할 수 있다
- 4긴급 hotfix 절차(main merge → develop merge)를 순서대로 실행할 수 있다
- 5잘못된 커밋에 git revert를 안전하게 적용하고 reset과의 차이를 설명할 수 있다
브랜치 전략 — GitFlow
GitFlow 브랜치 구조
브랜치 전략은 팀이 코드를 어떻게 나누고 합치는가의 규칙입니다. 규칙이 없으면 누가 어디에 무엇을 push했는지 파악이 어렵고, 긴급 수정이 필요할 때 경로가 없어 혼란이 생깁니다.
확대
운영 장애가 났습니다. 누군가 main에 직접 push한 커밋이 테스트 없이 배포됐고, 롤백하려니 "어느 커밋으로 돌아가야 하나"부터 막힙니다. 릴리즈 태그도 없고, 누가 무엇을 바꿨는지 추적할 방법이 없습니다. 브랜치 전략이 없는 팀은 긴급 상황에서 이렇게 무너집니다. 규칙 없이 모두가 한 브랜치에 push하면, 운영 코드와 개발 코드가 뒤섞이고 장애 원인을 찾는 데 배포 복구보다 더 많은 시간이 걸립니다. GitFlow는 소규모~중규모 팀에서 검증된 전략입니다. 브랜치별 역할이 명확하게 분리됩니다.
확대
브랜치 역할 요약:
| 브랜치 | 역할 | 직접 push 여부 |
|---|---|---|
main | 운영 배포 기준. 항상 배포 가능 상태 | 금지 (MR only) |
develop | 다음 릴리즈 통합 브랜치 | 금지 (MR only) |
feature/* | 기능 단위 개발 | 개인 작업 가능 |
release/* | 릴리즈 준비 (QA, 버그픽스) | 제한적 |
hotfix/* | 운영 긴급 수정 | main에서 분기 |
# feature 브랜치 생성 및 작업
git checkout develop
git pull origin develop
git checkout -b feature/user-logout
# 작업 후 커밋
git add .
git commit -m "feat: 사용자 로그아웃 기능 구현 (#45)"
# develop으로 MR(Merge Request) 요청
git push origin feature/user-logout
# GitLab에서 develop 대상 MR 생성
Trunk-Based Development — GitFlow의 대안
팀 규모가 작거나 배포 주기가 짧은 경우, 더 단순한 전략이 유리할 수 있습니다.
| 비교 항목 | GitFlow | Trunk-Based |
|---|---|---|
| 브랜치 수 | 많음 (5종) | 적음 (main + 단기 feature) |
| 릴리즈 주기 | 주/월 단위 | 수시(하루 여러 번 가능) |
| 팀 규모 | 중~대규모 | 소~중규모 |
| 적합한 상황 | 명확한 버전 관리 필요 | CI/CD 자동화가 잘 된 팀 |
이 모듈에서는 인프라 운영 팀에서 더 자주 보이는 GitFlow를 기준으로 다룹니다.
확대
태그 기반 릴리즈
git tag 로 배포 기준점 만들기
배포 기준점이 없으면 "어제 배포본으로 롤백해줘"라는 요청을 받았을 때 정확히 어느 커밋인지 찾기가 어렵습니다. git tag는 특정 커밋에 이름을 붙이는 방법입니다. 한 번 붙이면 브랜치와 달리 이동하지 않습니다.
# annotated tag 생성 (메시지 포함, 배포 이력에 남김)
git tag -a v1.2.0 -m "Release v1.2.0 — 로그아웃 기능 추가, 성능 개선"
# 원격 저장소에 push
git push origin v1.2.0
# 전체 태그 목록 확인
git tag -l
# 특정 버전 두 태그 사이의 커밋 목록
git log --oneline v1.1.0..v1.2.0
a3f9c21 feat: 사용자 로그아웃 기능 구현 (#45)
b82e1d3 fix: 세션 만료 시 리다이렉트 오류 수정 (#43)
c11a027 perf: 쿼리 캐시 레이어 추가 (#40)
배포 이력 추적 루틴:
# 가장 최근 태그 확인
git describe --tags --abbrev=0
# 특정 커밋이 어느 태그 기준인지 확인
git describe --tags a3f9c21
# 날짜 기준 배포 이력
git log --oneline --since="2026-01-01" --until="2026-06-01" --decorate | grep "tag:"
실습
태그 생성은 현재 브랜치의 최신 커밋에 붙습니다. main 브랜치에서 실행해야 운영 기준점으로 사용할 수 있습니다. 먼저 main 브랜치에 있는지 확인합니다.
# main 브랜치 확인
git branch --show-current
# 또는
git status | head -1
# main이 아니라면 이동
git checkout main
git pull origin main
# 태그 생성 및 push
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
git tag -a v1.0.0 -m 'Release v1.0.0' && git push origin v1.0.0- git log --oneline --decorate | head -5로 HEAD 위치를 먼저 확인 — HEAD -> main과 tag: v1.0.0이 같은 커밋 해시에 있어야 정상. 다른 커밋에 있으면 태그가 잘못된 커밋을 가리키는 것
- git show v1.0.0 출력에서 태그 타입 확인 — tag 오브젝트(Annotated)면 Tagger와 Date가 나옴. commit 오브젝트면 lightweight 태그로 메시지가 없어 릴리즈 추적에 부적합
- git push origin v1.0.0 후 GitLab Tags 탭에서 해당 태그가 보이지 않으면 — 원격에 push된 브랜치가 달라 태그 참조 커밋이 원격에 없는 경우. git push origin main 먼저 실행 후 태그 push
두 태그 사이의 변경 내역을 확인합니다. 릴리즈 노트 작성이나 롤백 범위 파악에 사용합니다. 이전 태그(v0.9.0)가 없는 경우 첫 커밋부터 확인하려면 git log --oneline v1.0.0을 씁니다.
# 이전 릴리즈 이후 커밋 목록
git log --oneline v0.9.0..v1.0.0
# 변경된 파일 목록
git diff --name-only v0.9.0..v1.0.0
# 작성자별 기여 요약
git shortlog v0.9.0..v1.0.0
a3f9c21 feat: 사용자 로그아웃 기능 구현 (#45)
b82e1d3 fix: 세션 만료 시 리다이렉트 오류 수정 (#43)
c11a027 perf: 쿼리 캐시 레이어 추가 (#40)
git log --oneline v0.9.0..v1.0.0- git log --oneline v0.9.0..v1.0.0 커밋 수를 먼저 확인 — 0개이면 두 태그가 같은 커밋을 가리키거나 태그 순서가 역전된 것. 50개 이상이면 릴리즈 단위가 너무 커서 롤백 범위 특정이 어려움
- 커밋 메시지에 feat/fix/perf prefix가 없으면 릴리즈 노트 자동 생성 불가 — 최소 50% 이상의 커밋이 컨벤션을 따라야 changelog 도구 활용 가능. 이슈 번호(#N) 없으면 추적 단절
- 커밋 목록이 맞는데 git diff --name-only 변경 파일이 예상보다 많으면 — 다른 브랜치의 변경이 병합된 것. merge commit이 포함됐는지 git log --merges로 확인
이슈-커밋 연결
커밋 메시지 컨벤션과 이슈 연결
커밋 메시지를 일관된 형식으로 작성하면, 나중에 "이 변경이 왜 생겼나"를 추적하기 쉬워집니다. GitLab은 커밋 메시지의 #이슈번호를 자동으로 해당 이슈와 연결합니다.
# 커밋 메시지 형식 권장
# <type>: <요약> (#이슈번호)
#
# type 종류:
# feat — 새 기능
# fix — 버그 수정
# docs — 문서
# refac — 리팩터링
# chore — 빌드/배포/설정 변경
# 예시
git commit -m "fix: 로그인 후 세션 쿠키 누락 수정 (#78)"
git commit -m "feat: Prometheus 메트릭 엔드포인트 추가 (#82)"
git commit -m "chore: application-prod.yml 로그 레벨 WARN으로 변경"
# 특정 이슈 번호가 언급된 커밋 찾기
git log --oneline --all | grep "#78"
# 최근 30개 커밋 확인
git log --oneline -30
# 특정 작성자 커밋만 확인
git log --oneline --author="minjun" --since="1 week ago"
# 특정 파일이 언제 마지막으로 변경됐는지
git log --oneline -5 -- src/main/resources/application-prod.yml
트러블슈팅
상황: 장애를 일으키는 커밋이 main에 merge됐고, 빠르게 되돌려야 합니다. git revert와 git reset 중 어느 것을 써야 하는지 판단 기준이 핵심입니다.
# 문제 커밋 SHA 확인
git log --oneline main | head -10
# 예: d9a3f1c feat: 캐시 로직 변경 (#91) ← 이 커밋이 문제
# 방법 1: git revert (공유 브랜치에서 항상 이 방법 사용)
# 문제 커밋을 취소하는 새 커밋을 만든다 — 이력 보존
git revert d9a3f1c
# 에디터가 열리면 커밋 메시지 확인 후 저장
git push origin main
# 방법 2: git reset --hard (주의: 공유 브랜치에서 사용 금지)
# 이 방법은 혼자 쓰는 feature 브랜치에서만
git reset --hard d9a3f1c~1 # 해당 커밋 이전으로 되돌림
# force push 필요 → 다른 팀원 이력 꼬임 발생 가능
판단 기준:
| 상황 | 사용할 명령 | 이유 |
|---|---|---|
| main/develop 등 공유 브랜치 | git revert | 이력 보존, force push 불필요 |
| 혼자 쓰는 feature 브랜치 | git reset 가능 | 아직 다른 사람이 pull하지 않음 |
| 이미 force push한 상황 | 팀에 알리고 revert | 이력 복구 시도보다 revert가 안전 |
원인: feature 브랜치와 develop 브랜치가 동일한 파일의 같은 부분을 각자 수정했습니다. Git이 자동으로 합칠 수 없어 충돌 마커를 삽입하고 멈춥니다.
# 충돌 파일 목록 확인
git status | grep "both modified"
# 충돌 마커 확인 (파일 안에 삽입됨)
grep -n "<<<<<<" src/main/resources/application.yml
# 충돌 마커 해석:
# <<<<<<< HEAD ← 현재 브랜치(develop)의 내용
# spring.datasource.url=jdbc:mysql://develop-db:3306/mydb
# =======
# spring.datasource.url=jdbc:mysql://localhost:3306/mydb_dev
# >>>>>>> feature/new-db-config ← merge하려는 브랜치 내용
# 직접 편집해서 올바른 내용만 남김 (마커 줄 포함 삭제)
vi src/main/resources/application.yml
# 수정 후 해결 완료 표시
git add src/main/resources/application.yml
# merge 완료
git commit
# (merge 커밋 메시지가 자동으로 채워짐)
# 충돌이 많으면 GUI 도구 활용
git mergetool # vimdiff, VS Code 등 설정에 따라 실행
충돌 예방 습관:
- feature 브랜치 작업이 길어지면 주기적으로
git merge develop으로 최신화 - 설정 파일 같이 충돌 잦은 파일은 MR 전 팀에 알리고 작업
심화 — 이력은 지워지지 않는다, 그리고 저장소 속의 저장소
심화: git은 스냅샷 이력을 영구 보관한다 — 대용량·비밀이 특별한 이유
"파일을 커밋했다가 다음 커밋에서 지웠으니 사라졌겠지"는 git에서 통하지 않습니다. git이 데이터를 어떻게 저장하는지 알면, 왜 대용량 파일과 비밀값을 특별히 다뤄야 하는지 보입니다.
- 모든 커밋은 완전한 스냅샷입니다: git은 diff가 아니라 각 커밋 시점의 파일 전체(blob)를 저장하고, 커밋들이 그 blob을 가리킵니다. 파일을 나중에 지워도 그 blob은 과거 커밋에서 여전히 도달 가능해 이력에 남습니다. 그래서 한 번 커밋된 100MB 덤프는 이후 삭제해도 모든 클론의
.git을 계속 무겁게 만듭니다. - 비밀도 마찬가지입니다: 비밀번호·키를 커밋했다가 다음 커밋에서 지워도
git log -p·git show로 과거 커밋에서 그대로 꺼낼 수 있습니다. 삭제 커밋은 "새 커밋을 추가"할 뿐 과거를 바꾸지 않습니다. 유출된 비밀은 이력 재작성과 별개로 반드시 회전(교체) 해야 합니다. - 대용량은 LFS로 분리합니다: 이미지·바이너리·빌드 산출물처럼 큰 파일은 Git LFS로 관리하면, 저장소엔 작은 포인터만 들어가고 실제 blob은 LFS 서버에 둡니다. 애초에 빌드 산출물은 커밋하지 않는 것이 원칙입니다.
- 한계/되돌리기: 이미 이력에 박힌 대용량·비밀을 진짜로 지우려면
git filter-repo(또는 BFG)로 이력을 재작성하고 강제 push한 뒤, 모두가 다시 클론해야 합니다 — 공유 브랜치 이력을 갈아엎는 무거운 작업이라, 처음부터 안 올리는 것이 훨씬 쌉니다.
그래서 .gitignore와 커밋 리뷰는 미관이 아니라, "영구 보관되는 것에 무엇을 넣을지"를 지키는 안전장치입니다.
상황: 저장소를 클론했더니 특정 디렉터리(공통 라이브러리)가 비어 있어 컴파일이 실패합니다. 혹은 로컬은 되는데 CI만 옛 버전으로 빌드돼, 이미 고친 버그가 되살아납니다.
원인: 그 디렉터리가 Git 서브모듈입니다. 서브모듈은 부모 저장소가 자식 저장소의 특정 커밋 SHA를 가리키는 포인터만 담고, 자식의 실제 파일은 담지 않습니다. 그냥 git clone만 하면 서브모듈 폴더는 비어 있고, git submodule update --init --recursive(또는 git clone --recursive)를 해야 그 SHA로 체크아웃됩니다. CI가 옛 버전으로 빌드되는 것은, 서브모듈 안에서 최신 커밋을 만들었지만 부모가 가리키는 SHA를 갱신·커밋하지 않아 부모가 여전히 옛 SHA를 가리키기 때문입니다.
진단: git submodule status로 부모가 가리키는 SHA와 실제 체크아웃된 SHA를 비교합니다. .gitmodules에서 경로·URL을 확인하고, CI 로그에서 서브모듈 초기화 단계가 도는지, --recursive가 빠졌는지 봅니다.
해결: 클론 측은 git submodule update --init --recursive로 폴더를 채웁니다. 버전을 올릴 땐 서브모듈 안에서 원하는 커밋으로 이동한 뒤 부모에서 그 변경(새 SHA 포인터)을 git add·커밋해 포인터를 갱신합니다. CI에는 체크아웃 단계에서 서브모듈을 재귀 초기화하도록 명시해(예: 체크아웃 옵션의 submodules recursive) 재발을 막습니다.
실제 업무에서 이 지식이 쓰이는 상황:
인프라/운영 엔지니어에게 형상관리가 실질적으로 필요한 세 장면입니다.
1. 긴급 핫픽스 절차 (운영 버그 발생 시):
# main 기반으로 hotfix 브랜치 생성
git checkout main
git pull origin main
git checkout -b hotfix/session-null-pointer
# 수정 후 커밋
git add .
git commit -m "fix: 세션 null 시 NPE 수정 (#101)"
# main에 merge (운영 배포)
git checkout main
git merge hotfix/session-null-pointer
git tag -a v1.2.1 -m "Hotfix v1.2.1 — 세션 NPE 긴급 수정"
git push origin main
git push origin v1.2.1
# develop에도 반영 (재발 방지)
git checkout develop
git merge hotfix/session-null-pointer
git push origin develop
# hotfix 브랜치 삭제
git branch -d hotfix/session-null-pointer
git push origin --delete hotfix/session-null-pointer
2. 배포 이력 추적:
# "3주 전 배포본으로 돌아가야 한다" 요청이 오면
git log --oneline --decorate | grep "tag: v"
# 릴리즈 태그가 있어야 정확한 기준점으로 배포 가능
# 특정 날짜 이전 마지막 태그 확인
git log --tags --simplify-by-decoration --pretty="format:%d %ai" | grep "v1\." | head -5
3. 설정 파일 변경 추적: 운영 서버 설정이 언제, 누가, 왜 바뀌었는지 조회합니다.
git log --oneline --follow -- config/application-prod.yml
git show <커밋SHA>:config/application-prod.yml
형상관리가 잘 된 팀은 장애 발생 시 "어떤 버전에서 문제가 생겼나"를 5분 안에 파악합니다.
명령어·단축키 빠른 참조
이 모듈에서 다룬 Git 브랜치·태그·롤백·서브모듈 명령을 실전 옵션과 함께 모았습니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
git tag -a | 릴리즈 기준점(annotated) 생성 | git tag -a v1.2.0 -m "Release v1.2.0" |
git push origin <태그> | 태그 원격 반영 | 브랜치 먼저 push 후 태그 push |
git describe --tags | 현재 커밋의 최근 태그 확인 | git describe --tags --abbrev=0 |
git log --oneline v0.9.0..v1.0.0 | 릴리즈 간 변경·롤백 범위 | git diff --name-only로 파일까지 |
git revert <SHA> | 공유 브랜치 안전 롤백(이력 보존) | main/develop은 항상 revert |
git reset --hard <SHA> | 로컬 브랜치 되돌리기(주의) | 혼자 쓰는 feature 브랜치만 |
git merge | 브랜치 통합 | 충돌 시 git mergetool·마커 편집 |
git status | grep "both modified" | 충돌 파일 식별 | 이후 git add로 해결 표시 |
git commit -m "<type>: 요약 (#N)" | 컨벤션 커밋·이슈 연결 | fix: 세션 쿠키 누락 (#78) |
git log --author --since | 작성자·기간별 이력 추적 | git log --oneline --author="minjun" |
git submodule update --init --recursive | 빈 서브모듈 폴더 채우기 | 클론 후 라이브러리 비면 실행 |
git submodule status | 부모가 가리키는 SHA 대조 | 옛 SHA면 부모에서 add·커밋 |
git log --follow -- <파일> | 설정 파일 변경 이력 조회 | git show <SHA>:config/application-prod.yml |
관련 모듈로 더 깊이:
- Maven/Gradle/npm 빌드와 산출물 관리 — Git에 올린 코드를 실제 배포 산출물로 만드는 빌드 단계
- WAR/JAR/정적파일 배포와 배포 스크립트 작성 — 태그 기반 릴리즈가 실제 배포 파이프라인으로 이어지는 구조
- dev/stg/prod 환경변수와 설정 파일 분리 전략 — 설정 파일 변경 추적과 환경별 구성 관리의 연결
다음 모듈에서는 이 코드를 실제 배포 산출물로 만드는 빌드 도구 실무를 다룹니다.