Git— 스냅샷으로 이해하는 버전 관리
8장

태그와 릴리스 워크플로

태그는 움직이지 않는 이름이다 — 브랜치가 커밋마다 앞으로 달려가는 동안, 태그는 특정 순간의 스냅샷에 영원히 고정된 표지판으로 남는다.

태그란 무엇인가 — 브랜치와의 결정적 차이

태그(tag)는 특정 커밋에 붙이는 불변 이름이다. 브랜치가 "지금 작업 중인 위치"를 가리키는 이동하는 포인터라면, 태그는 "이 순간을 기록한다"는 고정된 책갈피다.

브랜치와 태그는 내부적으로 모두 .git/refs/ 아래의 파일이지만 동작이 다르다. 브랜치 파일은 새 커밋이 생길 때마다 자동으로 새 SHA로 덮어쓰인다. 태그 파일은 한 번 만들어지면 수동으로 삭제하거나 강제로 이동시키지 않는 한 절대 바뀌지 않는다.

이 불변성이 릴리스 관리에서 태그를 쓰는 핵심 이유다. v1.2.0 태그를 찍으면 6개월 뒤에도, 100개의 커밋이 더 쌓인 뒤에도, 그 태그는 정확히 같은 커밋을 가리킨다. 브랜치로는 이런 보장을 할 수 없다.

태그(tag) — 특정 커밋을 가리키는 불변 참조. git commit을 실행해도 태그는 이동하지 않는다. 브랜치는 커밋할 때마다 자동으로 전진하지만, 태그는 그렇지 않다.
직관: 브랜치는 "지금 어디에 있는가"를 나타내는 움직이는 손가락이고, 태그는 역사책에 붙이는 "이날 이 사건이 일어났다"는 고정 메모지다.

경량 태그 vs 애너테이트 태그

Git에는 두 종류의 태그가 있다. 경량 태그(lightweight tag)는 단순한 포인터이고, 애너테이트 태그(annotated tag)는 자체 메타데이터를 가진 진짜 Git 객체다.

경량 태그는 커밋 SHA를 가리키는 파일 하나다. 태거 정보, 날짜, 메시지가 없다. git tag v1.0.0-beta처럼 만든다. 로컬 임시 표시나 개인 참조용으로는 충분하지만, 릴리스에는 적합하지 않다.

애너테이트 태그는 별도의 Git 객체(tag object)로 저장된다. 여기에는 태그 이름, 태거 이름과 이메일, 태그 날짜, 태그 메시지, 그리고 선택적으로 GPG 서명이 포함된다. git tag -a v1.0.0 -m "First stable release"로 만든다.

공식 릴리스에는 항상 애너테이트 태그를 사용해야 한다. GitHub · GitLab이 릴리스 페이지를 생성할 때 태그를 기반으로 하며, git describe 명령도 기본적으로 애너테이트 태그를 참조한다. 경량 태그는 git describe에서 --tags 옵션 없이는 무시된다.

GPG 서명 태그(git tag -s)는 공개키로 검증 가능한 서명을 포함한다. 오픈소스 프로젝트나 보안이 중요한 소프트웨어 배포에서 태그 진위 확인에 사용된다.

태그 생성과 관리
git tag v1.0.0                      # 경량 태그 (현재 HEAD)
git tag -a v1.0.0 -m "릴리스 메모"  # 애너테이트 태그 (권장)
git tag -s v1.0.0 -m "서명 릴리스"  # GPG 서명 태그
git tag -a v1.0.0 <sha>             # 과거 커밋에 태그
git tag -l                          # 태그 목록
git tag -l "v1.*"                   # 패턴으로 필터링
git show v1.0.0                     # 태그 정보 + 커밋 정보
git tag -d v1.0.0                   # 로컬 태그 삭제
git push origin v1.0.0              # 태그 push
git push origin --tags              # 모든 태그 push
git push origin :refs/tags/v1.0.0   # 원격 태그 삭제
함정: git push는 기본적으로 태그를 push하지 않는다. 태그를 원격으로 공유하려면 반드시 git push origin <태그이름> 또는 git push --tags를 별도로 실행해야 한다.

Semantic Versioning과 태그 명명 규칙

태그 이름은 자유롭지만, 대부분의 프로젝트는 Semantic Versioning(SemVer) 규칙인 v<MAJOR>.<MINOR>.<PATCH> 형식을 따른다.

SemVer의 세 숫자는 각각 의미가 다르다. MAJOR는 하위 호환성이 깨지는 변경(breaking change). MINOR는 하위 호환성을 유지하면서 새 기능 추가. PATCH는 하위 호환성을 유지하는 버그 수정.

이 규칙을 따르면 사용자는 태그 이름만으로 업그레이드 위험도를 가늠할 수 있다. v2.0.0으로 업그레이드할 때는 breaking change를 확인해야 하지만, v1.2.3에서 v1.2.4로의 업그레이드는 안전하다고 기대할 수 있다.

프리릴리스와 빌드 메타데이터도 SemVer에 포함된다: v1.0.0-alpha.1, v1.0.0-rc.2, v1.0.0-beta.3+build.456.

태그 이름에 v 접두사를 쓰는지 여부는 프로젝트마다 다르다. 중요한 것은 팀 안에서 일관성을 유지하는 것이다.

Semantic Versioning (semver.org)MAJOR.MINOR.PATCH 규칙. MAJOR: 하위 호환 불가 변경. MINOR: 하위 호환 기능 추가. PATCH: 하위 호환 버그 수정.

브랜치 전략과 릴리스 태그의 관계

태그는 브랜치 전략과 함께 사용될 때 진정한 위력을 발휘한다. 세 가지 주요 전략을 살펴보자.

트렁크 기반 개발(Trunk-Based Development)은 모든 개발자가 main(트렁크) 하나에 자주 — 하루에도 여러 번 — 통합하는 전략이다. 릴리스는 main의 특정 시점에 태그를 찍는 것으로 이루어진다. 긴 수명의 브랜치를 최소화하므로 통합 충돌이 적고, CI/CD와 잘 어울린다. 태그 패턴: main의 커밋에 v1.0.0 직접 태깅.

GitHub Flowmain이 항상 배포 가능한 상태를 유지하는 단순한 전략이다. 기능 개발은 짧은 피처 브랜치에서 하고, PR → main 병합이 이루어지면 즉시 배포한다. 배포 시점에 태그를 찍는다. 작은 팀과 빠른 릴리스 사이클에 적합하다.

Git Flow는 더 복잡한 구조를 갖는다. main(항상 프로덕션 상태), develop(다음 릴리스 통합), feature/*, release/*, hotfix/* 브랜치를 모두 운영한다. release/* 브랜치에서 QA를 마치면 maindevelop 양쪽에 merge하고 main에 버전 태그를 찍는다. hotfix/*main에서 직접 분기해 패치 후 다시 maindevelop에 병합한다.

어떤 전략을 선택하든 태그의 역할은 같다 — 배포 가능한 특정 커밋에 불변 이름을 붙여, 언제든 그 상태를 재현할 수 있게 한다.

핵심: 태그는 브랜치 전략의 "마침표"다. 어떤 전략을 쓰든 프로덕션에 나가는 커밋에는 태그를 찍어라. 6개월 뒤 "v1.3.2가 어떤 코드였지?"라는 질문에 즉시 답할 수 있어야 한다.

git describe — 태그 기반 버전 식별

git describe는 현재 커밋을 가장 가까운 태그를 기준으로 설명하는 사람이 읽을 수 있는 문자열을 생성한다. CI/CD 파이프라인에서 자동 버전 레이블을 생성할 때 매우 유용하다.

정확히 태그된 커밋에서 실행하면 태그 이름을 반환한다: v1.2.0. 태그 이후 3개의 커밋이 더 쌓인 커밋에서 실행하면: v1.2.0-3-g4a5b6c7. 이 형식은 "v1.2.0 태그로부터 3개의 커밋 후, SHA가 4a5b6c7인 커밋"을 의미한다.

이 자동 생성 버전 문자열을 빌드 아티팩트 이름이나 바이너리 버전 정보에 포함시키면, 어떤 코드로 빌드됐는지 항상 추적할 수 있다.

git describe 활용
git describe                    # 가장 가까운 애너테이트 태그 기준
git describe --tags             # 경량 태그 포함
git describe --always           # 태그가 없어도 SHA 반환
git describe --abbrev=8         # SHA를 8자리로 표시
# 빌드 시스템 활용 예:
VERSION=$(git describe --tags --always)
echo "Building version: $VERSION"

태그 시각화 — 브랜치와의 차이

아래 애니메이션은 커밋이 쌓이는 동안 태그가 어떻게 고정된 채로 남고 브랜치 포인터만 전진하는지, 그리고 릴리스 태그가 어떻게 특정 커밋을 영구적으로 표시하는지 보여준다.

태그의 불변성 — 브랜치는 전진하고 태그는 고정된다 1 / 4
SPACE 재생 · ← → 이동 · R 리셋
Git Flow 릴리스 태그 패턴 1 / 3
SPACE 재생 · ← → 이동 · R 리셋
← 충돌 해결과 스태시목차 →