| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 66챌린지
- Android ViewPager2
- Android
- Java
- Android Navigation
- Android 12 대응
- 알고리즘 자바
- 프로그래머스 알고리즘
- MVP Architecture
- DataBinding
- 안드로이드 카카오 로그인
- WebView
- 코틀린 코루틴
- Kotlin FCM
- Kotlin
- 영어독립365
- OkHttp Interceptor
- Android WebView
- 카카오 알고리즘
- 안드로이드
- Android ProgressBar
- scope function
- Android 12
- 안드로이드 갤러리 접근
- Android Jetpack
- 영어공부
- 습관만들기
- Android Interceptor
- coroutine
- android recyclerview
- Today
- Total
주맨의 개발노트
[Git] 브랜치를 바꾸지 않고 브랜치를 옮겨다니는 법 본문
브랜치를 바꾸려면 하던 일을 먼저 치워야 합니다. git stash로 밀어두고, 브랜치를 옮기고, 일을 끝내고, 다시 돌아와서 꺼냅니다. 익숙한 순서입니다.
그런데 Android 프로젝트에서는 이 왕복이 생각보다 비쌉니다. 브랜치를 옮기면 소스가 통째로 바뀌므로 Gradle sync가 다시 돌고, 증분 빌드 상태가 무효화되면서 다음 빌드에서 상당 부분을 다시 컴파일합니다. 세 줄 고치면 끝나는 핫픽스 앞뒤로 그보다 긴 시간이 붙습니다.
git worktree는 이 왕복 자체를 없애는 기능입니다. 브랜치를 바꾸는 대신 작업 디렉토리를 하나 더 두는 방식입니다. 다만 공짜는 아니고, Android에서는 치러야 할 것이 몇 가지 있습니다. 이 글에서는 worktree가 정확히 무엇을 격리하고 무엇을 격리하지 않는지, 그리고 Android 프로젝트에서 실제로 무엇을 치르게 되는지 정리합니다.
하나의 저장소, 여러 개의 작업 트리
git 저장소를 클론하면 두 가지가 함께 생깁니다.
| 구분 | 무엇 | 어디에 |
|---|---|---|
| 저장소 | 커밋, 브랜치, 태그, 리모트 설정, stash | .git/ |
| 작업 트리 | 지금 체크아웃되어 파일로 펼쳐진 상태 | 그 바깥 전부 |
우리가 평소 "저장소"라고 부르는 디렉토리는 사실 이 둘이 1:1로 붙어 있는 상태입니다. 브랜치를 바꾸면 작업 트리가 통째로 그 브랜치의 모습으로 갈아끼워집니다. 한 번에 하나만 볼 수 있는 이유가 여기 있습니다.
git worktree는 이 1:1을 1:N으로 바꿉니다. 저장소는 그대로 하나인데, 거기에 붙는 작업 트리를 여러 개 만듭니다.
git worktree add ../SampleProject-hotfix hotfix/login-crash
이제 디렉토리가 두 개입니다.
~/AndroidStudioProjects/
├── SampleProject/ ← 메인 체크아웃, feature/checkout 브랜치
│ └── .git/ ← 진짜 저장소는 여기 하나뿐
└── SampleProject-hotfix/ ← 추가된 작업 트리, hotfix/login-crash 브랜치
└── .git ← 디렉토리가 아니라 "파일"
주목할 것은 마지막 줄입니다. 추가된 작업 트리의 .git은 디렉토리가 아니라 파일이고, 그 안에는 진짜 저장소 위치를 가리키는 한 줄이 들어 있습니다.
cat ../SampleProject-hotfix/.git
gitdir: /Users/me/AndroidStudioProjects/SampleProject/.git/worktrees/SampleProject-hotfix
이 파일 하나가 worktree의 성질을 거의 다 설명합니다. 파일은 따로 있고, 저장소는 하나다.
무엇이 격리되고 무엇이 격리되지 않는가
worktree를 "저장소를 하나 더 만드는 것"으로 오해하기 쉬운데, 그렇지 않습니다. 격리되는 것은 파일뿐입니다.
| 대상 | 격리됨 | 공유됨 |
|---|---|---|
| 체크아웃된 파일 | ✅ | |
| 현재 브랜치 | ✅ | |
build/, .gradle/ 같은 산출물 | ✅ | |
| 커밋 이력 | ✅ | |
| 브랜치 목록 | ✅ | |
| 태그, 리모트 설정 | ✅ | |
| stash 스택 | ✅ | |
git config (로컬) | ✅ |
실무에서 부딪히는 것은 대개 아래 절반입니다. 특히 두 가지를 기억해두면 좋습니다.
stash는 공유된다
worktree A에서 git stash한 것이 worktree B의 git stash list에 그대로 보입니다. B에서 git stash pop을 하면 A가 치워둔 작업을 B의 작업 트리에 꺼내게 됩니다.
worktree를 여러 개 쓰기 시작하면 git stash pop은 위험한 명령이 됩니다. git stash push -m "설명"으로 이름을 붙이고 git stash apply stash@{n}으로 지정해서 꺼내는 습관이 필요합니다.
같은 브랜치를 두 곳에서 체크아웃할 수 없다
git worktree add ../SampleProject-test develop
fatal: 'develop' is already checked out at
'/Users/me/AndroidStudioProjects/SampleProject'
git이 막습니다. 한 브랜치를 두 작업 트리가 동시에 편집하면 어느 쪽이 진짜 상태인지 정의할 수 없기 때문입니다. 이건 제약이 아니라 worktree가 성립하기 위한 조건에 가깝습니다.
worktree는 파일 충돌을 없애지 git 충돌을 없애지 않습니다. 한쪽에서 브랜치를 지우면 다른 쪽에서도 사라집니다. 저장소는 하나이기 때문입니다.
실무에서 언제 쓰나
개념만 보면 "브랜치를 여러 개 열어둘 수 있다" 정도로 들리는데, 실제로 손이 가는 상황은 좀 더 구체적입니다. 두 가지만 보겠습니다.
기능 개발 중에 핫픽스가 들어올 때
가장 흔한 경우입니다. 화면 하나를 절반쯤 만들어놓았는데 프로덕션에서 크래시가 터졌습니다. 지금 브랜치는 컴파일도 안 되는 상태입니다.
worktree 없이 처리하면 이 순서를 밟게 됩니다.
git stash push -u -m "checkout 화면 작업 중"
git checkout -b hotfix/login-crash origin/main
# ... 수정, 빌드, 확인, PR
git checkout feature/checkout
git stash pop
명령 자체는 어렵지 않습니다. 문제는 Android에서 브랜치 전환이 비싸다는 것입니다. 전환할 때마다 소스가 통째로 바뀌므로 Android Studio가 Gradle sync를 다시 하고, 증분 빌드 상태가 무효화되면서 다음 빌드에서 상당 부분을 다시 컴파일합니다. 이 왕복을 두 번 하게 됩니다. 핫픽스 자체는 세 줄 고치는 일인데 앞뒤로 붙는 시간이 그보다 깁니다.
거기에 stash의 위험이 얹힙니다. -u를 빠뜨리면 새로 만든 파일이 남고, pop 시점에 충돌이 나면 그것도 풀어야 합니다.
worktree를 쓰면 전환이 아예 없습니다.
git worktree add ../SampleProject-hotfix -b hotfix/login-crash origin/main
cd ../SampleProject-hotfix
# ... 수정, 빌드, 확인, PR
cd ../SampleProject # 작업하던 상태 그대로
stash 왕복 2회, Gradle sync 2회, 증분 빌드 상태 무효화 2회. 여기에 -u 누락과 pop 충돌 위험이 따라옵니다.
원래 디렉토리를 손대지 않으므로 stash도 재sync도 없습니다. 대신 새 디렉토리에서 첫 빌드가 콜드 빌드입니다.
즉 한 번의 콜드 빌드와 두 번의 브랜치 전환 중 무엇이 싼가를 재는 문제로 바뀝니다. 핫픽스가 길어질수록 worktree 쪽이 유리해집니다.
동료의 PR을 실제로 돌려봐야 할 때
리뷰는 diff만 읽고 끝나지 않습니다. 특히 UI 변경이라면 실제로 설치해서 봐야 합니다. 그런데 리뷰하자고 내 작업 트리를 치우는 것은 부담스럽습니다.
git worktree add ../SampleProject-review -b review/pr-1234 origin/feature/new-onboarding
cd ../SampleProject-review
./gradlew installDebug
리뷰가 끝나면 디렉토리째 지웁니다. 코멘트를 남기다가 "내 쪽 구현은 어떻게 했더라"를 확인해야 할 때도 창을 두 개 띄워 나란히 볼 수 있습니다. 이건 브랜치 전환으로는 안 되는 일입니다. 한 트리에는 한 상태만 올라가기 때문입니다.
두 예시의 공통점
둘 다 "동시에 여러 개를 돌린다"가 아닙니다. 내 작업 트리를 그대로 둔 채 다른 상태가 잠깐 필요한 상황입니다.
그리고 worktree가 이득이 되는 조건도 같습니다. 브랜치 전환 비용이 큰 프로젝트일수록 이득이 큽니다. 전환이 즉시 끝나는 스택이라면 굳이 디렉토리를 늘릴 이유가 없습니다. Android는 전환할 때마다 sync와 재컴파일이 따라붙는 대표적인 경우이고, 멀티모듈에 KSP를 쓴다면 더 그렇습니다.
바꿔 말하면, 다음 절에서 볼 비용 항목들이 worktree를 쓰는 이유이기도 합니다. 브랜치를 바꿀 때 치를 것을 worktree를 만들 때 한 번만 치르고 마는 것입니다.
Android에서 치르는 비용
여기서부터가 Android 개발자에게 실질적인 부분입니다. worktree를 하나 추가한다는 것은 Gradle 프로젝트를 하나 더 갖는 것과 같습니다.
콜드 빌드
새 worktree에서 첫 빌드는 콜드 빌드입니다. 다만 흔히 오해하는 것과 달리, 의존성을 다시 받지는 않습니다.
| 캐시 | 위치 | worktree 간 |
|---|---|---|
| 의존성, Gradle 배포판, 빌드 캐시 | ~/.gradle (GRADLE_USER_HOME) |
공유 |
| configuration cache, 증분 빌드 상태 | 프로젝트 .gradle/ |
각자 |
| 컴파일 산출물, 리소스, dex | build/, **/build/ |
각자 |
즉 네트워크는 다시 타지 않고, 다시 컴파일합니다. 기존 체크아웃이라면 바뀐 파일만 처리하면 되는 것을, 새 worktree에서는 Kotlin 컴파일부터 애노테이션 처리(KSP/KAPT), 리소스 병합, dex까지 전부 처음부터 돌립니다.
콜드 빌드 자체는 Android만의 일이 아닙니다. 어떤 언어든 빌드 산출물 디렉토리가 비어 있으면 처음부터 빌드합니다. Android의 사정은 그 단계가 유독 많고 길다는 것입니다. 멀티모듈에 KSP를 쓰는 프로젝트라면 이 차이가 특히 크게 벌어집니다.
여기서 중요한 판단이 하나 있습니다. 이 비용은 worktree를 만드는 순간이 아니라 거기서 빌드를 돌리는 순간 발생합니다. 새 트리를 만들어놓고 빌드하지 않으면 치를 것이 없습니다. worktree를 어떤 용도로 쓸 것인지에 따라 비용 계산이 완전히 달라진다는 뜻입니다.
Android Studio
Android Studio는 worktree를 별도 프로젝트로 취급합니다. 두 트리를 동시에 보려면 창을 두 개 띄워야 하고, 새 창은 이 두 가지를 처음부터 다시 합니다.
Gradle sync
모듈 구조와 의존성 그래프를 다시 구성합니다. 멀티모듈 프로젝트일수록 오래 걸립니다.
인덱싱
심볼 테이블을 다시 만듭니다. 이게 끝나기 전에는 코드 점프도 자동완성도 리팩터링도 동작하지 않습니다.
여기에 IntelliJ 프로세스 하나와 Gradle 데몬 하나가 통째로 더 뜹니다. 인덱스도 각자 갖습니다. 실질적으로 가장 크게 체감되는 비용은 시간보다 메모리입니다.
그리고 .idea/ 아래의 실행 구성(Run Configuration)은 대체로 gitignore 대상이라 새 트리에는 없습니다. 두 번째 창에서는 실행 구성을 다시 만들어야 합니다.
다만 이 비용도 조건부입니다. worktree를 IDE로 열지 않으면 하나도 발생하지 않습니다. 터미널에서 CLI로 빌드만 한다면 창을 띄울 일이 없습니다.
Gradle 데몬
두 트리에서 동시에 빌드하면 ~/.gradle의 캐시 파일 락을 두 빌드가 함께 잡으려 하면서 한쪽이 잠깐 대기할 수 있습니다. 다만 Gradle은 이 상황을 정상 시나리오로 설계했고, 실제 체감은 대체로 짧습니다. 진짜 비용은 데몬이 하나 더 떠서 JVM 힙을 그만큼 더 먹는 것 쪽입니다.
git에 없지만 빌드에는 필요한 파일들
worktree를 만들면 git은 자기가 추적하는 파일만 새 디렉토리에 펼칩니다. 당연한 이야기입니다. git은 자기가 모르는 파일을 복사할 수 없기 때문입니다.
그런데 Android 프로젝트에는 git이 일부러 모르게 해둔 파일 중에 빌드에 필요한 것이 섞여 있습니다. .gitignore에 올려둔 파일들입니다. 이 파일들은 메인 체크아웃에만 있고 새 worktree에는 없습니다.
git clone을 새로 받았을 때와 같은 상태라고 보면 됩니다. 저장소를 새로 받으면 local.properties부터 만들어야 하는 것처럼, worktree도 똑같이 만들어줘야 합니다. 같은 프로젝트 디렉토리 옆에 생기다 보니 이 사실을 놓치기 쉬울 뿐입니다.
| 파일 | 왜 gitignore인가 | 없으면 |
|---|---|---|
local.properties |
SDK 경로가 개발자마다 다름 | 모든 빌드가 실패 |
keystore.properties, *.jks |
서명 키는 저장소에 올리면 안 됨 | release 빌드만 실패 |
API 키가 든 *.properties |
비밀값 | 빌드는 되는데 런타임에 실패 |
세 행의 실패 시점이 전부 다르다는 점이 중요합니다. 아래로 갈수록 늦게 터지고, 늦게 터질수록 원인을 worktree와 연결하기 어려워집니다.
local.properties
가장 먼저 부딪히는 것은 첫 행입니다. 새 worktree에서 빌드하면 이런 메시지가 나옵니다.
SDK location not found. Define a valid SDK location with an ANDROID_HOME
environment variable or by setting the sdk.dir path in your project's
local properties file at '.../SampleProject-hotfix/local.properties'.
메시지 자체는 친절합니다. 어느 경로의 어떤 파일이 필요한지 다 적혀 있습니다. 그런데도 한 번쯤 헤매게 되는 이유는, 방금 전까지 잘 빌드되던 프로젝트라는 인식 때문입니다. 같은 저장소, 같은 코드인데 SDK를 못 찾는다니 프로젝트 설정이나 Gradle 쪽을 먼저 의심하게 됩니다. 실제 원인은 훨씬 단순합니다. 디렉토리가 바뀌었고, 그 디렉토리에는 그 파일이 없습니다.
참고로 ANDROID_HOME 환경변수가 설정된 환경이라면 local.properties 없이도 빌드가 됩니다. 이 문제를 한 번도 겪지 않는 사람이 있는 이유입니다.
대처
worktree를 만든 직후에 필요한 파일을 복사하면 됩니다.
git worktree add ../SampleProject-hotfix -b hotfix/login-crash origin/main
cp SampleProject/local.properties ../SampleProject-hotfix/
복사할 것이 여러 개라면 한 줄로 묶어두는 편이 낫습니다.
# gitignore된 것 중 빌드에 필요한 것만
for f in local.properties keystore.properties; do
cp "$f" ../SampleProject-hotfix/ 2>/dev/null
done
이 문제는 Android만의 것은 아닙니다. 어느 스택이든 저장소에 올리지 않는 로컬 설정 파일이 있고, 새 체크아웃에는 그것이 없습니다. 다만 Android는 그중 하나가 빌드 자체를 막는다는 점이 다릅니다. 다른 스택에서는 대개 실행 시점에 드러나는데, 여기서는 컴파일도 시작하지 못합니다.
어디에 둘 것인가
worktree 디렉토리의 위치는 자유입니다. 크게 두 가지 방식이 있습니다.
저장소 바깥에 둡니다. .gitignore를 건드릴 일이 없고, Android Studio의 프로젝트 목록에도 나란히 보입니다. git 공식 문서의 예제가 쓰는 방식입니다.
프로젝트 하나에 관련된 것이 한 디렉토리에 모입니다. 대신 .gitignore에 반드시 추가해야 합니다. 빠뜨리면 메인 체크아웃의 git status가 worktree 안의 파일로 뒤덮입니다.
git worktree add ../SampleProject-hotfix hotfix/login-crash
~/AndroidStudioProjects/
├── SampleProject/
└── SampleProject-hotfix/
저장소 안에 두기로 했다면 .gitignore 등록을 잊지 마세요.
.worktrees/
Android 프로젝트에서는 하나 더 확인할 것이 있습니다. settings.gradle.kts가 모듈을 자동 탐색하는 구성이라면, 저장소 안에 중첩된 worktree가 모듈로 잡히지 않는지 봐야 합니다. Android의 기본 템플릿은 include(":app")처럼 명시적으로 나열하므로 대개 문제없지만, 커스텀 탐색 로직을 넣었다면 확인이 필요합니다.
명령 정리
# 새 브랜치로 만들기
git worktree add ../SampleProject-feature -b feature/new-screen
# 특정 커밋에서 새 브랜치로 만들기
git worktree add ../SampleProject-hotfix -b hotfix/login-crash origin/main
# 기존 브랜치로 만들기
git worktree add ../SampleProject-review review/pr-1234
# 목록 (어느 디렉토리에서 실행해도 전체가 나옴)
git worktree list
# 제거 — 디렉토리를 rm으로 지우지 말 것
git worktree remove ../SampleProject-feature
# 변경사항이 남아 있어 거부될 때
git worktree remove --force ../SampleProject-feature
# 디렉토리를 이미 손으로 지워버렸을 때, 등록 정보 정리
git worktree prune
rm -rf로 디렉토리를 지우면 .git/worktrees/ 아래 등록 정보가 남아서 git worktree list에 유령처럼 남습니다. git worktree prune으로 정리되지만, 처음부터 git worktree remove를 쓰는 편이 낫습니다.
정리하며
worktree는 저장소를 복제하지 않습니다. 파일만 격리하고 커밋, 브랜치, stash, 리모트는 전부 공유합니다. 이 한 문장을 기억하면 대부분의 동작이 설명됩니다. stash가 공유된다는 것도, 같은 브랜치를 두 곳에서 체크아웃할 수 없다는 것도 여기서 나옵니다.
Android에서 치르는 비용은 콜드 빌드와 IDE 창 하나입니다. 그런데 둘 다 조건부라는 점이 중요합니다. 그 트리에서 빌드하지 않고 IDE로 열지 않으면 발생하지 않습니다.
그래서 worktree가 비싼 도구인지 싼 도구인지는 거기서 무엇을 할 것인가에 따라 갈립니다. 두 브랜치를 나란히 빌드해서 비교하려는 목적이라면 비쌉니다. 반면 핫픽스나 리뷰처럼 잠깐 열었다 닫는 목적이라면, 실제로 치르는 것은 한 번의 콜드 빌드와 디스크 공간뿐입니다. 그 대가로 브랜치 전환 왕복이 사라집니다.
브랜치를 바꿀 때마다 sync와 재컴파일을 기다리고 있었다면, 그 비용을 한 번만 치르고 마는 선택지가 있다는 뜻입니다.
- worktree는 파일만 격리합니다. 커밋, 브랜치 목록, stash, 리모트 설정은 모두 하나의
.git을 공유합니다. - stash 공유에 주의하세요. 다른 worktree에서 밀어둔 작업이 목록에 보이므로
git stash pop대신apply stash@{n}을 쓰는 편이 안전합니다. - 의존성은 다시 받지 않습니다.
~/.gradle은 공유되고, 새로 생기는 것은 프로젝트.gradle/과build/입니다. 네트워크가 아니라 재컴파일 비용입니다. - 비용은 조건부입니다. 콜드 빌드는 그 트리에서 빌드할 때, IDE 비용은 창을 띄울 때 발생합니다.
local.properties를 챙기세요. gitignore된 파일은 새 worktree에 복사되지 않고, 그중 하나는 빌드 자체를 막습니다.- 브랜치 전환이 비싼 프로젝트일수록 이득이 큽니다. Android는 전환마다 Gradle sync와 증분 빌드 무효화가 따라붙는 대표적인 경우입니다.
'Git' 카테고리의 다른 글
| [CD] Release Drafter로 릴리즈 노트 자동화 (0) | 2026.06.07 |
|---|---|
| [Git] git stash란 (0) | 2022.10.17 |