블로그 CI가 20분 걸렸던 이유 — 캐시를 제거해서 해결하기까지
증상
블로그 글 PR을 머지한 뒤 stag에 새 글이 20분 이상 반영되지 않았다. Flux image automation은 GHCR에 새 이미지가 올라오면 약 2분 안에 stag를 업데이트한다. 배포 자체가 느린 게 아니라 CI가 느린 것이었다.
gh run list --workflow "Blog CI/CD"를 확인하면 main push 빌드가 1223분이고, PR 빌드는 12분이었다.
PR 빌드가 빠른 이유
워크플로우에 if: github.ref == 'refs/heads/main' 조건이 있어서 PR 빌드는 lint, typecheck, test만 실행한다. 1~2분은 예상된 동작이다.
docker job 시간 분해
실제 빌드(PR #1051, run 27283634090)를 타임스탬프로 분해한 결과:
| 단계 | 소요 시간 | 비고 |
|---|---|---|
| GHA 캐시 import | 70초 | mode=max 캐시 다운로드 |
| COPY 레이어 × 63개 | ~3초 | workspace package.json 전체 |
RUN bun install |
14초 | 988 패키지 |
RUN bun run build |
3초 | markdown + bundle + Tailwind |
GHCR 이미지 push (#77) |
27초 | TLS timeout retry 포함 |
GHA 캐시 export (#78) |
468초 (7분 48초) | ← 진짜 병목 |
이미지 push는 27초에 끝났다. 나머지 약 8분은 GHA 캐시를 내보내는 시간이었다.
mode=max가 역효과인 이유
cache-to: type=gha,mode=max는 oven/bun:1 베이스 이미지 전체를 포함한 모든 중간 빌드 레이어를 GHA 캐시 서버에 업로드한다. 수백 MB를 매 빌드마다 저장하는 것이다.
캐시 히트로 아낄 수 있는 최대 시간은 bun install 14초다.
캐시 오버헤드는:
- import: 70초 (캐시 미스 시 전부 낭비)
- export: 468초
완벽한 캐시 히트라 해도 계산은:
14초 절약, 70초 + 468초 소비 = 524초 손해
게다가 캐시 미스도 자주 발생했다. 첫 번째 실질 레이어가 COPY package.json bun.lock ./인데, 모노레포에서 어떤 앱이든 새 패키지를 추가하면 bun.lock이 변경되어 이 레이어가 무효화된다. 활발히 개발 중인 프로젝트에서는 거의 매 머지마다 캐시 미스가 난다.
해결
blog.yml에서 cache-from과 cache-to를 제거:
- cache-from: type=gha,scope=${{ github.workflow }}
- cache-to: type=gha,mode=max,scope=${{ github.workflow }}
이제 bun install은 항상 실행되지만(14초), 538초의 캐시 오버헤드가 사라진다. docker job 총 소요 시간: 10분 이상 → 2분 이내.
교훈
GHA 캐시 mode=max는 전체 빌드 그래프를 저장한다. 느린 빌드 단계가 안정적으로 캐시될 때는 유용하지만, 캐시 대상 단계가 14초짜리일 때는 오버헤드가 거의 항상 이득을 초과한다.
빌드 캐시를 설정하기 전에 두 가지를 측정해야 한다:
- 캐시 히트 시 무엇을 아끼나? (느린 단계를 실측)
- 캐시 import + export에 얼마나 걸리나? (job 로그를 읽기)
오버헤드가 절약량보다 크다면, 캐시가 CI를 빠르게 만드는 게 아니라 느리게 만들고 있는 것이다.
