
org.gradle.caching=true 한 줄만 켜면 빌드가 빨라질 줄 알았는데, 오히려 조회 오버헤드만 늘더라고요. 캐시는 켜는 게 아니라 '맞추는' 거였습니다.
작년 하반기에, 사이드로 붙잡고 있던 안드로이드 멀티 모듈 프로젝트가 있었어요. 코드 두세 줄만 고쳐서 깃허브(GitHub)에 올려도 CI가 전체 모듈을 처음부터 다시 컴파일했습니다. PR 하나 올리면 체크가 16분씩 돌았어요. 커피 한 잔 내리고 와도 아직 빌드 중이었지요.
Gradle 빌드 캐시 동작 원리부터 짚고 가기
Gradle은 태스크의 모든 입력을 해싱해서 캐시 키를 만듭니다. 소스 파일, classpath, 컴파일러 버전까지 전부 들어가요. 이 키가 같으면 예전 출력을 그대로 꺼내 쓰고, 1비트라도 다르면 캐시 미스로 처리해서 결과를 다시 만듭니다.
캐시는 두 종류예요. 로컬 캐시는 기본으로 켜져 있어서 내 PC 안에서만 재사용됩니다. 원격 캐시는 기본이 꺼져 있는데, 팀 전체가 공유하는 중앙 저장소이지요. 조회 순서는 로컬을 먼저 보고, 없으면 원격을 뒤진 다음, 원격에서 찾으면 로컬에도 저장해두는 흐름입니다.
여기서 제가 처음에 놓친 게 있었어요. 캐시를 켜는 것 자체는 시작일 뿐이고, 진짜 승부는 적중률에서 갈리더라고요. 히트율이 낮으면 캐시를 뒤지느라 시간만 더 쓰게 됩니다.

캐시 적중률 떨어뜨리는 3대 캐시 킬러
clean 빌드를 두 번 돌려봤는데 두 번째에도 FROM-CACHE 로그가 안 떴어요. 원인을 파고들어 보니 매 빌드마다 입력값이 미세하게 바뀌고 있었습니다. 대표적인 범인이 세 가지였어요.
1. 절대 경로
빌드 머신마다 프로젝트가 놓인 경로가 다릅니다. 제 PC는 C:\dev\..., CI는 /home/runner/... 이런 식이죠. 태스크 입력에 절대 경로가 그대로 들어가면 머신이 바뀔 때마다 무조건 캐시 미스예요. 태스크 입력에 상대 경로 정규화(relative path normalization)를 적용해서 경로 차이를 지워야 합니다.
2. 타임스탬프
이게 제일 자주 밟는 지뢰였어요. BuildConfig에 빌드 시각을 박아 넣거나, Properties.store()가 파일 맨 위 주석에 생성 시각을 자동으로 찍는 경우가 있거든요. 빌드할 때마다 시각이 달라지니 입력도 매번 바뀝니다. 저는 빌드 시각 대신 Git 커밋 해시를 넣는 쪽으로 바꿨어요. 같은 커밋이면 같은 값이 나오니까요.
3. 비결정적 출력
같은 입력을 넣어도 결과가 매번 달라지는 애노테이션 프로세서 같은 녀석들입니다. 이건 프로세서 자체 문제라 우회가 까다로운 편이에요. 어떤 태스크가 범인인지부터 특정하는 게 먼저입니다.

caching.debug로 캐시 미스 원인 추적하기
범인 찾는 방법은 의외로 단순했어요. 빌드 명령에 -Dorg.gradle.caching.debug=true를 붙이면 캐시 키를 만들 때 쓴 모든 입력 속성이 로그로 쏟아집니다.
미스가 난 두 빌드의 로그를 각각 뽑아서 diff로 비교하면, 어떤 입력 하나가 달라졌는지 딱 잡힙니다. 저는 이 방법으로 타임스탬프가 매번 바뀌던 걸 잡아냈어요. 눈으로 추측하지 말고 로그를 diff 뜨는 게 제일 빠르더라고요.

팀 원격 캐시는 'CI 쓰기, 개발자 읽기'가 핵심
로컬 캐시가 자리를 잡았으면 원격 캐시로 넘어갈 차례예요. 여기서 정책 하나가 정말 중요합니다. CI는 원격 캐시에 쓰기만, 개발자 PC는 읽기만 하도록 나눠야 해요.
이유가 있어요. 개발자 PC는 IDE가 파일을 건드리거나 환경이 제각각이라 빌드 결과가 오염될 여지가 있는 편이에요. 그 오염된 결과가 공유 캐시에 올라가면 팀 전체가 잘못된 출력을 받게 됩니다. CI 환경이 훨씬 예측 가능하고 깨끗하니, 쓰기 권한은 CI한테만 주는 게 안전하다고 봅니다.
깃허브 액션(GitHub Actions)을 쓴다면 gradle/actions/setup-gradle이 로컬 빌드 캐시까지 복원해주는데요. 원격 캐시로 push하는 구조라면 액션의 로컬 캐시 복원 기능은 꺼두는 게 효율적입니다. 안 그러면 로컬이랑 원격을 이중으로 챙기게 되니까요.
CI 빌드 16분에서 6분으로, 캐시가 만능은 아닌 이유
정리하고 나니 제 프로젝트 기준으로 CI 캐시 적중률이 34%에서 87%까지 올라갔고, 16분씩 걸리던 PR 체크가 6분 아래로 떨어졌어요. 6분이 어느 정도냐면, 커피 내리러 갔다 오면 이미 끝나 있는 시간이거든요.
다만 이 효과가 모든 프로젝트에 똑같이 나온다고 보긴 어렵습니다. 빌드가 원래 빠르거나 적중률이 낮은 구조면 조회 오버헤드만 늘 수 있어요. 태스크 입력 선언이 누락되면 오래된 결과를 재사용하다 엉뚱한 버그가 생길 위험도 있고요. 원격 캐시는 서버 운영 비용도 따라옵니다.
빌드 캐시는 여러 최적화 도구 중 하나일 뿐입니다. 모듈 분리, 컨피규레이션 캐시(org.gradle.configuration-cache=true), 병렬 빌드를 같이 굴려야 효과가 제대로 붙어요. 특히 빌드 캐시는 태스크 '출력'을 아끼고, 컨피규레이션 캐시는 '구성 계산' 단계를 아끼는 식으로 절감 구간이 달라서, 둘을 병행하면 겹치지 않게 시간이 빠집니다.
지금 CI가 매번 처음부터 다 빌드하고 있다면, 캐시 킬러 세 개 중 어느 놈이 내 빌드를 매번 새것으로 만들고 있을까요. caching.debug 로그를 한 번 diff 떠보면, 적중률 절반을 좌우하는 그 한 놈이 대개 거기 찍혀 있습니다.

'Android 개발 > Gradle・빌드' 카테고리의 다른 글
| 초록불 뒤에 숨은 JAR: Gradle 의존성 검증 실전 삽질기 (0) | 2026.09.12 |
|---|---|
| 빌드가 5분으로 늘어났는데 범인을 모를 때, Build Analyzer로 태스크를 잡았습니다 (0) | 2026.09.05 |
| 월요일마다 손으로 올리던 의존성 PR, Renovate로 자동화한 기록 (1) | 2026.08.17 |
| KMP AGP 9 마이그레이션, 안드 앱 모듈부터 손본 빌드 복구 일지 (0) | 2026.08.14 |
| CI에서만 죽는 Gradle OutOfMemory, -Xmx 를 줄여야 살아나는 이유 (0) | 2026.07.21 |