Published on

GitHub Actions 캐시 안 먹힘 원인 7가지

Authors

서로 다른 러너에서 반복 실행되는 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/)를 통째로 캐시하지 않기
- 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') }}

실전 점검 체크리스트 (로그만 보고 빠르게)

  1. Cache key:가 실행마다 바뀌나? (원인 1)
  2. path가 실제로 존재하고 파일이 쌓이나? (원인 2)
  3. 잡이 끝까지 성공해서 Post job cleanup까지 가나? (원인 3)
  4. fork PR인가? 이벤트/권한이 제한되나? (원인 4)
  5. 매트릭스인데 키에 환경 차이를 포함했나? (원인 5)
  6. 캐시 대상이 재현 불가 산출물로 오염됐나? (원인 6)
  7. 정책/용량/정리로 캐시가 날아간 건 아닌가? (원인 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 불일치 해결도 같이 참고해 보세요.