홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

캐시를 껐더니 CI가 빨라졌습니다 — 최적화가 손해가 되는 조건

정기창·2026년 8월 15일

6편으로 시리즈를 끝냈다고 생각했습니다. 노트북 두 대는 껐고, 쿼터를 태우던 범인은 잡았고, 사지 않은 물건 목록까지 정리했습니다. 그런데 5편에 지나가듯 적어둔 문장 하나가 계속 걸렸습니다.

self-hosted에서는 캐시가 손해일 수 있다

"수 있다"가 걸렸습니다. 그건 측정한 문장이 아니라 짐작한 문장입니다. 그리고 저는 바로 그 편에서 그럴듯한 짐작에 한 번 속은 참이었습니다.

그때 제가 본 것은 숫자 하나였습니다

5편에서 제가 가진 근거는 "캐시 업로드에만 5분"이라는 한 줄이 전부였습니다. 거기서 저는 이렇게 추론했습니다. 내 러너는 계속 살아 있으니 받아둔 파일이 디스크에 그대로 있을 텐데, 이미 가진 것을 굳이 압축해서 올리고 다시 받는 것은 손해 아니겠냐고.

논리는 지금 봐도 맞습니다. 다만 논리가 맞는 것과 그것이 실제로 얼마짜리인지 아는 것은 다른 일입니다. 손해라면 얼마나 손해인지, 정말 손해가 맞기는 한지, 저는 모르고 있었습니다. 그래서 다시 재기로 했습니다.

5일치 기록을 단계별로 쪼갰습니다

이번에는 작업 하나의 총 시간이 아니라 그 안의 단계별 시간을 봤습니다. CI 실행 기록에는 각 단계가 언제 시작해서 언제 끝났는지가 남아 있으니, 그것만 모으면 됩니다. 닷새치, 실행 134회, 작업 634개를 모아 단계별 중앙값을 냈습니다.

단계 GitHub hosted 자가 러너
의존성 캐시 복원 15초 (편차 6) 242초 (편차 119, 0~679초)
의존성 설치 17초 13초

여기서 두 가지가 눈에 들어왔습니다.

편차가 범인을 가리키고 있었습니다

첫 번째는 편차입니다. 자가 러너의 캐시 복원은 중앙값이 242초인데 실제 값은 0초에서 679초 사이를 널뛰었습니다. hosted는 13초에서 17초 사이에 얌전히 모여 있습니다.

이 흩어짐 자체가 단서였습니다. 계산이 병목이면 시간이 이렇게 흩어지지 않습니다. 같은 기계가 같은 계산을 하니 대체로 같은 시간이 걸립니다. 반대로 회선을 타는 작업은 그때그때 상황에 따라 몇 배씩 달라집니다.

5편에서 저는 평균값 하나를 놓고 범인을 지목했다가 틀렸습니다. 이번에는 평균이 아니라 흩어진 모양이 답을 알려줬습니다. 숫자 하나로 줄여버리면 사라지는 정보가 있다는 생각이 들었습니다.

정작 설치는 제 기계가 더 빨랐습니다

두 번째가 더 흥미로웠습니다. 의존성 설치 자체는 자가 러너가 hosted보다 빨랐습니다. 13초 대 17초입니다.

저는 그동안 "자가 러너는 느리다"를 하나의 덩어리로 생각하고 있었습니다. 그런데 쪼개보니 제가 이기는 구간이 분명히 있었습니다. 느렸던 것은 설치가 아니라 설치를 빠르게 하려고 붙여둔 장치였습니다.

그러니까 이 러너는 일을 못해서 느린 게 아니라, 일을 시작하기 전에 이미 자기가 가진 물건을 택배로 다시 받느라 4분을 쓰고 있었던 것입니다.

캐시가 어떤 전제 위에 서 있는지

왜 이런 일이 생기는지는 캐시라는 장치가 무엇을 가정하고 만들어졌는지를 보면 분명해집니다.

GitHub이 빌려주는 기계는 작업이 시작될 때 새로 만들어지고 끝나면 사라집니다. 그러니 매번 처음부터 받아야 하고, 그래서 한 번 받은 것을 어딘가 보관해뒀다가 다음번에 꺼내오는 장치가 이득이 됩니다. 이것이 캐시입니다.

그런데 제 기계는 사라지지 않습니다. 어제 받은 것이 오늘도 디스크에 그대로 있습니다. 이 상태에서 캐시는 "이미 가진 것을 압축해서 올리고 다음에 다시 내려받는" 절차가 됩니다. 게다가 그 왕복이 가정용 회선을 탑니다.

최적화는 자기가 서 있는 전제를 함께 상속합니다. 전제가 바뀌면 같은 장치가 반대로 작동할 수 있는데, 장치는 자기 전제가 깨졌다고 알려주지 않습니다. 그냥 조용히 손해를 냅니다.

고친 것은 조건식 한 줄이었습니다

해결은 간단했습니다. 캐시를 아예 없애는 게 아니라 hosted에서 돌 때만 켜면 됩니다. 러너가 자기 종류를 알려주는 값이 있으니 그걸로 갈랐습니다.

- uses: actions/setup-node@v4
  with:
    node-version: 22
    # self-hosted 에선 캐시 복원이 순수 손해 — store 가 이미 그 머신에 영구히 있다
    cache: ${{ runner.environment == 'github-hosted' && 'pnpm' || '' }}

hosted 경로는 이 표현식이 그대로 pnpm으로 평가되므로 종전과 완전히 동일합니다. 지금 제 CI는 hosted가 기본이니, 이 변경은 평상시 운영에 아무 영향이 없고 쿼터가 바닥났을 때 넘어가는 안전망의 실효성만 올립니다.

작업 시간이 이렇게 바뀌었습니다

작업 변경 전 변경 후
프론트엔드 E2E 568초 326초
백엔드 테스트 388초 148초
배포 도구 빌드 350초 42초
데스크톱 앱 빌드 348초 99초

마지막 줄은 즉시 효과가 났습니다. 그 작업은 원래부터 제 기계에서만 돌던 것이라 hosted로 돌아온 뒤에도 계속 캐시를 다시 받고 있었기 때문입니다. 제가 모르는 사이에 매일 4분씩 버려지고 있었습니다.

돌아보며

이 편에서 제가 한 일은 새로운 것을 알아낸 게 아니라 이미 적어둔 짐작을 숫자로 바꾼 것이 전부입니다. 그런데 그 과정에서 두 가지가 따라왔습니다.

하나는 짐작이 맞긴 맞았는데 제가 생각한 것보다 훨씬 컸다는 것입니다. "손해일 수 있다" 정도로 적어둔 것이 작업당 4분이었습니다. 짐작으로 남겨뒀다면 그 4분은 계속 흐르고 있었을 것입니다.

다른 하나는 짐작으로 남겨두는 동안 그것이 무해해 보였다는 점입니다. 틀린 결론이 아니라 그냥 확인하지 않은 결론이었으니까요. 다만 확인하지 않은 채로는 아무것도 바뀌지 않았습니다. 애석하게도 저는 이걸 시리즈를 마감했다고 선언한 뒤에야 재봤습니다.

그리고 이 측정이 예상하지 못한 곳으로 저를 데려갔습니다. 단계를 쪼개서 보는 눈이 생기자, 5편에서 내린 진단 하나가 다시 이상해 보이기 시작했습니다. 그때 저는 윈도우 러너가 느린 범인을 파일시스템으로 지목하고 만족했는데, 사실 그 편에는 제가 설명하지 못한 채 넘어간 관측이 하나 남아 있었습니다. 다음 편에서 그것을 다시 열어봅니다.

self-hosted runnerGitHub ActionsCI캐시성능 측정pnpm

관련 글

러너가 느린 범인을 Wi-Fi로 지목했다가 틀렸습니다 — 워밍이 콜드보다 느렸던 이유

윈도우 러너가 hosted보다 15배 느렸습니다. 공유기가 100Mbps라는 증거까지 찾아내 네트워크를 범인으로 지목했는데, 캐시가 채워진 상태의 재측정이 콜드보다 느리면서 진단이 통째로 뒤집혔습니다.

관련도 96%

다 만들어놓고 전부 껐습니다 — 쿼터를 태운 범인은 macOS 10배 배수였습니다

사용량을 총량으로만 보면 원인이 안 보입니다. 저장소별 SKU로 분해하자 raw 99.4분짜리 macOS 작업이 994분으로 청구돼 쿼터의 절반을 태우고 있었습니다. 2주간 만든 러너 함대는 그래서 전부 껐습니다.

관련도 96%

CI가 전부 멈췄고, 저는 원인을 확인하지 않았습니다 — 30분 만에 끝난 대응의 함정

GitHub Actions hosted 러너가 새벽에 전면 차단됐습니다. 변수 한 줄과 self-hosted 러너로 30분 만에 우회했지만, 에러 메시지를 원인으로 착각한 탓에 진짜 이유는 2주 뒤에야 알게 됐습니다.

관련도 95%