Gradle 프로젝트를 GitHub Actions로 CI/CD할 때 알아야 할 캐시의 모든 것.
세 가지 캐시 계층의 차이, 실무 적용 전략, 그리고 Self-hosted Runner에서의 함정까지.
1. 캐시는 세 층위로 나눠서 본다
Gradle CI에서 "캐시"라는 단어는 맥락에 따라 완전히 다른 세 가지를 가리킨다. 이걸 구분하지 않으면 설정이 꼬이고, 중복 캐싱으로 오히려 느려지거나, 디스크/메모리 낭비가 발생한다.

| 구분 | actions/cache@v4 | gradle/actions/setup-gradle | Gradle Build Cache |
|---|---|---|---|
| 정체 | GitHub의 범용 CI 캐시 | Gradle 전용 캐시 래퍼 | Gradle 자체 기능 |
| 캐싱 단위 | 디렉터리 단위 파일 스냅샷 | Gradle User Home 내 핵심 파일 | task input/output 해시 기반 |
| 캐싱 대상 | 지정한 path 전체 | dependency, wrapper, local build cache, compiled script 등 | 개별 task의 실행 결과물 |
| 키 전략 | 직접 설계 (key + restore-keys) | 자동 관리 | Gradle이 task input 해시로 자동 관리 |
| 저장 위치 | GitHub 서버 (원격) | GitHub 서버 (원격) | 로컬 디스크 또는 Remote Cache 서버 |
| immutable | O (같은 키 덮어쓰기 불가) | O (GitHub cache 기반이므로 동일) | X (같은 input이면 같은 output) |
핵심 차이를 한 문장으로:
- actions/cache@v4 → "이 디렉터리를 통째로 저장해뒀다가 다음에 복원해줘"
- setup-gradle → "Gradle에 필요한 것들만 골라서 저장/복원할게 (내부적으로 actions/cache 씀)"
- Gradle Build Cache → "이 task는 input이 안 바뀌었으니 실행 자체를 생략하고 이전 output을 쓸게"
2. actions/cache@v4 — 범용 파일 캐시
동작 원리
GitHub Actions의 범용 캐시다. 지정한 path를 tar로 압축 → GitHub 서버에 업로드 → 다음 실행 시 key 기준으로 복원한다.
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
키 매칭 순서
key가 정확히 일치하는 캐시를 먼저 찾는다- 없으면
restore-keys의 prefix로 매칭, 가장 최근 캐시를 가져온다 - 현재 브랜치 → 기본 브랜치(main/master) 순서로 탐색한다
핵심 제약
- Immutable: 한 번 저장된 캐시는 같은 키로 덮어쓸 수 없다. 키가 이미 존재하면 Post step에서 저장을 스킵한다.
- Branch Scope: 캐시는 생성된 브랜치와 기본 브랜치에서만 접근 가능하다. feature-A에서 만든 캐시를 feature-B에서 직접 가져올 수 없다.
- 용량 제한: 리포지토리당 총 10GB. 초과 시 오래된 캐시부터 eviction된다.
- 보존 기간: 7일간 미사용 시 자동 삭제.
한계
| 한계 | 설명 |
|---|---|
| 수동 경로 지정 | 어떤 디렉터리를 캐싱할지 직접 알아야 한다 |
| 키 전략 직접 설계 | hashFiles 대상, restore-keys prefix를 직접 설계해야 한다 |
| Gradle 내부 구조 무관심 | 캐시가 비대해져도 알아서 정리해주지 않는다 |
| 분기 정책 수동 | read-only/write 제어를 직접 조건문으로 처리해야 한다 |
3. gradle/actions/setup-gradle — Gradle 전용 캐시 래퍼
정체
GitHub 공식 Gradle 가이드에서도 권장하는 액션이다. 내부적으로 actions/cache를 사용하되, Gradle에 최적화된 캐싱 전략을 자동으로 적용한다.
캐싱 대상
setup-gradle이 자동으로 캐싱하는 항목:
| 항목 | 설명 |
|---|---|
| Gradle Distribution | 다운로드한 Gradle 바이너리 |
| Dependency Cache | ~/.gradle/caches/modules-* 등 다운로드된 의존성 |
| Wrapper Distribution | gradle-wrapper.jar |
| Local Build Cache | ~/.gradle/caches/build-cache-* |
| Compiled Build Scripts | .gradle/ 내 컴파일된 스크립트 |
| Transformed JARs | desugaring 등 변환된 JAR 파일 |
기본 동작
- uses: gradle/actions/setup-gradle@v5
이 한 줄이면:
- 워크플로우 시작 시 GitHub cache에서 Gradle 관련 상태를 복원
- 빌드 완료 후 변경된 상태를 GitHub cache에 저장
- 기본 브랜치에서만 cache write, 나머지 브랜치는 read-only (캐시 오염 방지)
주요 옵션
| 옵션 | 기본값 | 용도 |
|---|---|---|
cache-disabled: true |
false | 캐싱 완전 비활성화 |
cache-read-only: true |
비기본 브랜치에서 true | 캐시 읽기만, 저장 안 함 |
cache-write-only: true |
false | 복원 없이 저장만 (캐시 시드 용도) |
cache-cleanup |
on-success | 미사용 파일 정리. always, on-success, never |
cache-overwrite-existing: true |
false | Self-hosted runner에서 기존 캐시 덮어쓰기 허용 |
cache-encryption-key |
- | Configuration Cache 암호화 키 (Gradle 8.6+) |
중복 사용 금지 규칙
Gradle 공식 문서가 명시적으로 경고하는 조합:
setup-gradle + actions/cache@v4 (Gradle 경로 대상) → 충돌/비효율
setup-gradle + actions/setup-java의 cache: gradle → 중복 캐싱
setup-gradle을 쓰면 Gradle 관련 캐시는 전부 setup-gradle에 맡기고, actions/cache@v4는 Gradle과 무관한 경로(SonarQube, Docker layer 등)에만 사용한다.
4. Gradle Build Cache — task output 재사용
동작 원리
앞의 두 캐시가 "파일을 저장/복원"하는 거라면, Gradle Build Cache는 task 실행 자체를 생략하는 메커니즘이다.

활성화
# gradle.properties
org.gradle.caching=true
또는 CLI에서:
./gradlew build --build-cache
로컬 vs 리모트
| 구분 | Local Build Cache | Remote Build Cache |
|---|---|---|
| 위치 | ~/.gradle/caches/build-cache-* |
HTTP 기반 원격 서버 |
| 범위 | 해당 머신에서만 | 조직 내 모든 빌드 머신/개발자 |
| 설정 | 기본 활성화 (caching=true 시) | settings.gradle.kts에서 별도 설정 |
| 대표 서비스 | - | Develocity (구 Gradle Enterprise), 자체 HTTP 캐시 서버 |
Remote Build Cache 설정 예시:
// settings.gradle.kts
buildCache {
local {
isEnabled = true
}
remote<HttpBuildCache> {
url = uri("https://cache.example.com/cache/")
isPush = (System.getenv("CI") != null) // CI에서만 push
credentials {
username = System.getenv("CACHE_USER")
password = System.getenv("CACHE_PASS")
}
}
}
권장 패턴
| 역할 | Local Cache | Remote Cache |
|---|---|---|
| CI (main 빌드) | read + write | push (캐시 populate) |
| CI (PR 빌드) | read + write | read-only (오염 방지) |
| 로컬 개발자 | read + write | read-only (CI가 만든 캐시 재사용) |
이 패턴이면 개발자가 ./gradlew build를 실행할 때, CI에서 이미 빌드한 task output을 그대로 재사용할 수 있다. 모듈이 많을수록 효과가 극적이다.
5. Configuration Cache — 빌드 스크립트 파싱 캐싱
Build Cache와 별개 계층이다. Build Cache가 Execution Phase의 task output을 캐싱한다면, Configuration Cache는 Configuration Phase의 task 그래프 구성 결과를 캐싱한다.

활성화
./gradlew build --configuration-cache
또는 gradle.properties:
org.gradle.configuration-cache=true
주의사항
- Configuration Cache에는 credential, 환경변수 등 민감 정보가 포함될 수 있다
- GitHub Actions cache에 저장할 때
cache-encryption-key를 반드시 설정해야 한다 (Gradle 8.6+) - 모든 플러그인이 Configuration Cache를 지원하지는 않는다. 비호환 플러그인이 있으면 빌드가 실패한다
- 첫 적용 시 모든 모듈에서 로컬 검증 후 CI에 적용하는 게 안전하다
6. "GitHub Cache = Remote Cache"라고 할 때 주의할 점
이 표현이 두 가지 의미로 혼용된다.
A. GitHub Actions Cache를 원격 저장소처럼 쓰는 경우
setup-gradle이 GitHub Actions cache에 Gradle 상태를 저장/복원하므로, CI 관점에서는 "원격에 저장되는 캐시"처럼 느껴진다. 하지만 이건 워크플로우 간 파일 스냅샷 공유이지, Gradle의 정식 remote build cache가 아니다.
제약:
- Immutable (같은 키 덮어쓰기 불가)
- Branch scope 제한
- 리포지토리당 10GB 한도
- task 단위가 아닌 디렉터리 단위
B. Gradle의 진짜 Remote Build Cache
HTTP 기반으로 task output 자체를 조직 차원에서 공유하는 구조다. CI가 캐시를 populate하고, 다른 에이전트나 개발자가 읽어서 task 실행을 생략한다.
장점:
- Task input 해시 기반이라 변경된 task만 정확히 갱신
- Mutable (같은 해시면 같은 output)
- Branch 제한 없음
- 용량 제한은 캐시 서버 설정에 따름
GitHub Actions cache ≈ "CI 실행 간 파일 저장소"
Gradle Remote Build Cache ≈ "조직 차원 task output 공유 서버"
둘은 겹치는 부분이 있지만 완전히 같지 않다. 소규모 프로젝트에서는 setup-gradle만으로 충분하지만, 모듈 수가 많고 빌드 시간이 긴 조직에서는 별도 Remote Build Cache 도입이 효과적이다.
7. 실무 권장 구성
기본형 — 대부분의 Gradle 프로젝트에 적합
name: CI
on:
pull_request:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
# cache: gradle ← setup-gradle 쓸 때는 이 옵션 비활성화
- uses: gradle/actions/setup-gradle@v5
- name: Build
run: ./gradlew clean build
Self-hosted Runner — 로컬 캐시 우선 전략
Self-hosted runner는 로컬 디스크에 이전 빌드의 캐시가 남아있다. 매번 GitHub cache에서 수백 MB를 다운로드/압축해제하는 건 낭비다.
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
# 로컬 캐시 존재 여부 확인
- name: Check local Gradle cache
id: local-cache
run: |
if [ -d ~/.gradle/caches ] && [ "$(ls -A ~/.gradle/caches 2>/dev/null)" ]; then
echo "hit=true" >> $GITHUB_OUTPUT
du -sh ~/.gradle/caches ~/.gradle/wrapper 2>/dev/null || true
else
echo "hit=false" >> $GITHUB_OUTPUT
fi
# 로컬 캐시가 없을 때만 GitHub cache에서 복원
- name: Restore Gradle cache from GitHub
if: steps.local-cache.outputs.hit != 'true'
uses: actions/cache/restore@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Build
run: ./gradlew build --build-cache --configuration-cache
env:
GRADLE_OPTS: >
-Dorg.gradle.jvmargs="-Xms256m -Xmx1024m -XX:MaxMetaspaceSize=384m"
# 빌드 성공 시 GitHub cache에 저장 (로컬 캐시 백업 용도)
- name: Save Gradle cache
if: github.ref == 'refs/heads/main' && steps.local-cache.outputs.hit != 'true'
uses: actions/cache/save@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
이 전략의 핵심:
- 로컬 캐시가 있으면 → GitHub cache 다운로드 완전 스킵 (수십 초 절약)
- 로컬 캐시가 없으면 (서버 재기동 직후 등) → GitHub cache에서 복원 (Cold Start 완화)
- main 브랜치에서만 GitHub cache에 저장 (캐시 오염 방지)
Remote Build Cache까지 도입하는 경우
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Build
run: ./gradlew build --build-cache
env:
GRADLE_BUILD_CACHE_URL: ${{ secrets.BUILD_CACHE_URL }}
GRADLE_BUILD_CACHE_USER: ${{ secrets.BUILD_CACHE_USER }}
GRADLE_BUILD_CACHE_PASS: ${{ secrets.BUILD_CACHE_PASS }}
// settings.gradle.kts
buildCache {
local { isEnabled = true }
remote<HttpBuildCache> {
url = uri(System.getenv("GRADLE_BUILD_CACHE_URL") ?: "https://cache.example.com/cache/")
isPush = (System.getenv("CI") != null && System.getenv("GITHUB_REF") == "refs/heads/main")
credentials {
username = System.getenv("GRADLE_BUILD_CACHE_USER") ?: ""
password = System.getenv("GRADLE_BUILD_CACHE_PASS") ?: ""
}
}
}
8. 조합별 판단 기준
| 상황 | 권장 조합 | 이유 |
|---|---|---|
| 일반 Gradle 프로젝트 | setup-gradle 단독 |
캐시 전략 자동 관리, 별도 설정 불필요 |
| Self-hosted Runner | 로컬 캐시 우선 + actions/cache@v4 폴백 |
네트워크 다운로드 회피, Cold Start 대응 |
| 모노레포 / 대규모 멀티모듈 | setup-gradle + Remote Build Cache |
task 단위 캐싱으로 정밀한 재사용 |
| Gradle 외 도구 캐시 | actions/cache@v4 별도 사용 |
SonarQube, Docker layer 등 비-Gradle 경로 |
| 캐시 디버깅 | setup-gradle에 cache-disabled: true + 수동 actions/cache@v4 |
캐시 동작을 직접 제어하면서 문제 추적 |
9. 자주 하는 실수와 함정
실수 1: setup-gradle + actions/cache + setup-java의 cache:gradle 삼중 적용
# 이러면 안 된다
- uses: actions/setup-java@v5
with:
cache: gradle # ← 캐시 1
- uses: gradle/actions/setup-gradle@v5 # ← 캐시 2
- uses: actions/cache@v4 # ← 캐시 3
with:
path: ~/.gradle/caches
같은 디렉터리를 세 군데서 캐싱하면 저장/복원이 중복되고, 캐시 용량을 3배로 소모한다.
실수 2: Self-hosted Runner에서 매번 GitHub cache 다운로드
Self-hosted runner는 워크플로우가 끝나도 로컬 디스크가 유지된다. 그런데 actions/cache@v4나 setup-gradle을 기본 설정으로 쓰면 매번 GitHub에서 캐시를 다운로드하고 압축 해제한다. 로컬에 이미 있는 파일을 원격에서 또 받는 셈이다.
→ 로컬 캐시 존재 여부를 먼저 확인하고, 없을 때만 GitHub cache에서 복원하는 2단계 전략을 사용한다.
실수 3: 캐시 키에 자주 변하는 값 포함
# 이러면 캐시 히트율이 떨어진다
key: gradle-${{ runner.os }}-${{ github.sha }}
github.sha는 커밋마다 바뀌므로 매번 새 캐시를 만든다. hashFiles로 실제 빌드 설정 파일의 변경을 추적하는 게 맞다.
실수 4: clean build로 Build Cache 무력화
./gradlew clean build # clean이 이전 빌드 output을 지우므로 Build Cache 효과 감소
CI에서 clean을 쓰는 습관이 있다면, Build Cache가 활성화된 상태에서는 재고해볼 필요가 있다. clean은 로컬 프로젝트 빌드 디렉터리(build/)를 지우는 거지 Gradle User Home의 캐시(~/.gradle/caches/build-cache-*)를 지우는 건 아니지만, incremental build의 이점은 사라진다. Build Cache가 제대로 동작하면 clean 없이도 항상 올바른 결과를 보장한다.
실수 5: Configuration Cache에 민감정보 노출
Configuration Cache는 빌드 스크립트 파싱 결과를 직렬화한다. 환경변수, credential 등이 포함될 수 있다. GitHub Actions cache에 저장될 때 암호화하지 않으면 캐시를 통해 민감정보가 노출될 수 있다.
→ cache-encryption-key를 반드시 설정한다 (Gradle 8.6+):
- uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
실수 6: 동일 캐시를 여러 워크플로우에서 동시에 다운로드
Self-hosted runner 1대에서 여러 워크플로우가 순차 실행될 때, 각 워크플로우가 동일한 캐시를 개별적으로 다운로드/압축해제한다. 디스크 I/O가 반복되고, 이전 워크플로우의 프로세스 메모리가 해제되지 않은 상태에서 메모리가 누적되면 OOM이 발생할 수 있다.
→ 로컬 캐시 우선 전략으로 해결하거나, concurrency 그룹을 사용해 동시 실행을 제한한다.
10. 정리
┌─────────────────────────────────────────────────────────────┐
│ 캐시 계층 구조 요약 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [GitHub Actions cache] ← 파일 스냅샷. 범용. │
│ ↑ │
│ [setup-gradle] ← Gradle 전용 래퍼. 위를 내부 사용. │
│ ↑ │
│ [Gradle Build Cache] ← task output 재사용. 가장 깊은 층. │
│ ↑ │
│ [Configuration Cache] ← 빌드 스크립트 파싱 결과 캐싱. │
│ │
└─────────────────────────────────────────────────────────────┘
- Gradle 프로젝트 CI 기본값:
setup-java+setup-gradle+./gradlew build - Gradle User Home을
actions/cache@v4로 직접 캐싱: 특별한 이유 없으면 비추천 - 추가 도구 캐시 (SonarQube, Docker 등):
actions/cache@v4별도 사용 - 빌드 최적화가 중요:
org.gradle.caching=true+ 필요 시 Remote Build Cache 검토 - Self-hosted Runner: 로컬 캐시 우선 + GitHub cache 폴백의 2단계 전략
한 문장으로: GitHub Actions의 actions/cache@v4는 범용 파일 캐시이고, setup-gradle은 그 위에 Gradle 전용으로 최적화된 래퍼이며, Gradle의 Build Cache는 task output 재사용이라는 근본적으로 다른 계층의 메커니즘이다.