- Published on
GitHub Actions 캐시 안 먹힘 원인 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 러너에서 반복 실행되는 CI는 캐시가 먹히느냐/안 먹히느냐에 따라 빌드 시간이 몇 배씩 달라집니다. 그런데 actions/cache를 넣어도 로그에 Cache not found for input keys만 보이거나, 분명 저장한 것 같은데 다음 실행에서 복원이 안 되는 경우가 흔합니다.
이 글은 GitHub Actions 캐시가 “안 먹히는” 상황을 7가지 대표 원인으로 분해하고, 각 원인별로 로그에서 확인할 포인트 + 수정 예시를 제공합니다. (캐시는 만능이 아니라서, 원인을 정확히 찌르는 게 핵심입니다.)
> 참고로 장애 원인 분해/체크리스트 방식이 익숙하다면, 비슷한 접근으로 정리한 글도 함께 보면 도움이 됩니다: Spring Security OAuth2 401 - JWKS 캐시·kid 불일치 해결, AWS ALB 502·504 난사 - 원인별 해결 체크리스트
1) 캐시 키가 매번 바뀐다 (hashFiles/런ID/타임스탬프 오염)
가장 흔한 원인입니다. 캐시 키에 변동성이 큰 값이 섞이면 매 실행이 “새 캐시”가 되어 복원되지 않습니다.
흔한 실수
key: ${{ runner.os }}-${{ github.run_id }}처럼 run_id를 넣음key에 날짜/시간을 넣음hashFiles()대상이 너무 넓어서(예:**/*) 커밋마다 변함
개선 패턴
- 의존성 lockfile(예:
package-lock.json,pnpm-lock.yaml,yarn.lock,poetry.lock,gradle.lockfile)만 해시 restore-keys로 “근사치” 복원 허용
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
로그 체크
Cache key:가 실행마다 달라지는지 확인hashFiles결과가 의도치 않게 자주 바뀌는지 확인
2) path가 잘못됐거나, 실제로는 비어 있다 (저장할 게 없음)
캐시는 “경로에 있는 파일”을 저장합니다. 즉, 캐시 대상 경로가 실제로 존재하지 않거나 비어 있으면, 저장/복원이 의미가 없습니다.
흔한 실수
path: node_modules인데 실제 작업 디렉토리가frontend/node_modules- 빌드가 실패/스킵되어 의존성 설치가 실행되지 않음
~/.cache같은 경로를 썼는데 툴이 다른 경로에 저장
진단용 스텝 추가
- name: Inspect cache paths
run: |
pwd
ls -al
du -sh node_modules || true
du -sh ~/.npm || true
개선 예시 (서브디렉토리 프로젝트)
defaults:
run:
working-directory: frontend
- uses: actions/cache@v4
with:
path: |
frontend/node_modules
~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }}
3) 캐시 생성 시점이 늦거나, 잡이 중간에 실패한다 (저장이 안 됨)
actions/cache는 **복원(restore)**은 스텝 시작 시점에 하고, **저장(save)**은 잡 종료 시점(정확히는 포스트 액션)에서 수행됩니다. 즉, 잡이 중간에 실패/취소되면 저장이 안 될 수 있습니다.
흔한 패턴
- 테스트 실패로 잡이 종료 → 캐시가 저장되지 않음
if: always()를 잘못 써서 설치 스텝이 아예 실행되지 않음
해결 접근
- 최소한 “의존성 설치”까지는 실패하지 않도록 분리
- 캐시가 중요한 잡은 테스트 잡과 분리하거나, 실패해도 설치/빌드 산출물이 남는 구조로
jobs:
deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- run: npm ci
test:
needs: deps
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
4) PR/브랜치 맥락에서 캐시 접근이 제한된다 (fork PR, 권한 문제)
특히 fork에서 들어온 PR은 보안상 토큰 권한이 제한되고, 캐시 접근/저장이 기대대로 동작하지 않을 수 있습니다.
체크 포인트
- 이벤트가
pull_request인지pull_request_target인지 - PR이 fork에서 왔는지 (
github.event.pull_request.head.repo.fork)
권장 대응
- fork PR에서는 캐시 저장을 꺼서(또는 최소화해서) 예측 가능하게 운영
- 내부 브랜치/메인 브랜치에서만 캐시 저장
- uses: actions/cache@v4
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
> 보안 이슈로 pull_request_target을 무작정 쓰는 건 위험합니다. (외부 PR 코드가 권한 높은 컨텍스트에서 실행될 수 있음)
5) 동시 실행/매트릭스에서 캐시가 서로 덮어쓰거나 경합한다
매트릭스 빌드(예: node 18/20, OS별)에서 키가 동일하면 서로 캐시를 덮어쓰거나, 저장 경합으로 의도치 않은 결과가 납니다.
흔한 실수
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}- node 버전이 달라도 키가 같음
해결
- 환경 차이를 키에 포함 (node 버전, 아키텍처 등)
strategy:
matrix:
node: [18, 20]
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node${{ matrix.node }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node${{ matrix.node }}-npm-
${{ runner.os }}-npm-
6) 캐시할 대상이 “빌드 산출물”인데, 재현성이 없거나 오염된다
의존성 캐시(~/.npm, ~/.m2, ~/.gradle/caches)는 비교적 안정적이지만, dist/, .next/, build/ 같은 빌드 산출물 캐시는 재현성이 떨어져 캐시가 “먹히는 듯하다가” 자주 깨집니다.
증상
- 복원은 되지만 빌드가 실패하거나, 결과가 이상함
- 캐시가 커지고 복원 시간이 길어져 오히려 손해
권장
- 산출물 캐시는 툴이 공식 지원하는 캐시만 사용
- 예: Next.js는
~/.npm+.next/cache정도만
- 예: Next.js는
- 산출물 전체(
.next/)를 통째로 캐시하지 않기
- uses: actions/cache@v4
with:
path: |
~/.npm
.next/cache
key: ${{ runner.os }}-next-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx', '**/*.js') }}
restore-keys: |
${{ runner.os }}-next-${{ hashFiles('package-lock.json') }}-
위처럼 소스 해시를 섞으면 캐시 적중률이 떨어질 수 있으니, 팀 상황에 맞게 조절하세요(핵심은 “재현성 없는 디렉토리를 크게 캐시하지 말자”).
7) 캐시 정책/제한(크기, 보관, eviction)으로 인해 사라진다
캐시는 영구 저장소가 아닙니다. 조직/리포 정책, 사용량, GitHub의 내부 정책에 의해 오래된 캐시가 정리(eviction) 될 수 있습니다.
증상
- 며칠 전까지 잘 되다가 갑자기 miss
- 키는 동일한데도
Cache not found
대응
- 캐시가 꼭 필요하면, 복원 실패 시에도 정상 동작하도록 설계(캐시는 “가속”이지 “의존”이 아님)
- 키를 너무 세분화해 캐시 개수를 폭증시키지 않기
- 캐시 크기를 줄이기(불필요한 디렉토리 제외)
- uses: actions/cache@v4
with:
path: |
~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
실전 점검 체크리스트 (로그만 보고 빠르게)
Cache key:가 실행마다 바뀌나? (원인 1)path가 실제로 존재하고 파일이 쌓이나? (원인 2)- 잡이 끝까지 성공해서
Post job cleanup까지 가나? (원인 3) - fork PR인가? 이벤트/권한이 제한되나? (원인 4)
- 매트릭스인데 키에 환경 차이를 포함했나? (원인 5)
- 캐시 대상이 재현 불가 산출물로 오염됐나? (원인 6)
- 정책/용량/정리로 캐시가 날아간 건 아닌가? (원인 7)
예시: Node + npm 캐시를 “안전하게” 구성한 워크플로
아래 예시는 키 안정성, restore-keys, 매트릭스 분리, 경로 검증을 한 번에 반영한 템플릿입니다.
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Restore npm cache
uses: actions/cache@v4
with:
path: |
~/.npm
key: ${{ runner.os }}-node${{ matrix.node }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node${{ matrix.node }}-npm-
- name: Install deps
run: npm ci
- name: Test
run: npm test
- name: Debug cache dir size (optional)
if: always()
run: du -sh ~/.npm || true
마무리
GitHub Actions 캐시는 “설정했는데 왜 안 먹지?”가 아니라, 대부분 키/경로/실행 맥락/동시성 중 하나에서 논리적으로 설명됩니다. 위 7가지를 순서대로 훑으면 대개 10분 안에 원인이 좁혀집니다.
비슷한 방식으로 ‘캐시 불일치’ 문제를 디버깅하는 관점은 인증/토큰 캐시에서도 그대로 적용됩니다. 필요하면 Spring Security OAuth2 401 - JWKS 캐시·kid 불일치 해결도 같이 참고해 보세요.