사전 빌드 캐시
개념

빌드 캐시

gabury1고친 사람 github-actions[bot]

빌드 캐시는 한 번 해 둔 빌드 작업을 다시 하지 않게 해 줍니다. 빌드의 한 단계를 돌리기 전에 입력이 전과 같은지 먼저 봅니다. 입력이 전과 같은 단계는 돌리지 않습니다. 그때 만든 결과를 꺼내 씁니다. 팀이 함께 쓰는 캐시에서는 동료가 이미 만든 결과도 받아 씁니다.

쉽고 빠른 이해

빌드 캐시는 빌드 결과를 보관해 두었다가 같은 일이 다시 오면 꺼내 주는 창고입니다. 어제 컴파일한 모듈을 오늘 한 줄도 안 고쳤다면, 오늘 빌드는 그 모듈을 컴파일하지 않고 어제 결과를 가져옵니다.

이게 없으면 빌드는 같은 일을 몇 번이고 되풀이합니다. 브랜치를 오가거나 빌드 폴더를 지우면 멀쩡한 결과도 처음부터 다시 만듭니다. 매번 빈 기계에서 시작하는 빌드 서버(지속적 통합 서버)는 늘 전부 다시 합니다.

어떻게 도나:

  1. 단계마다 입력을 모아 짧은 값 하나로 줄입니다. 이 값이 결과를 찾는 열쇠입니다
  2. 그 열쇠로 창고를 찾아봅니다
  3. 있으면 결과를 꺼내 씁니다. 없으면 단계를 돌린 뒤 결과를 열쇠와 함께 넣어 둡니다

대가도 있습니다. 결과를 쌓아 둘 디스크가 듭니다. 팀이 함께 쓰는 창고라면 결과를 주고받는 시간도 듭니다. 열쇠를 만들 때 빠뜨린 입력이 있으면 낡은 결과를 맞는 줄 알고 꺼내 씁니다.

돌리는 데 오래 걸리는 단계가 같은 입력으로 자주 되풀이될 때 이득이 큽니다. 꺼내는 시간이 돌리는 시간보다 긴 단계나 배포처럼 바깥을 바꾸는 단계는 캐시에서 뺍니다.

상세

식당 주방은 같은 레시피에 같은 재료를 넣으면 늘 같은 소스가 나온다는 것을 압니다. 그래서 한 번 끓인 소스에 레시피 이름과 재료 목록을 적은 쪽지를 붙여 냉장고에 넣어 둡니다. 같은 쪽지로 주문이 들어오면 새로 끓이지 않고 냉장고에서 꺼냅니다.

빌드 캐시도 이렇게 돕니다. 쪽지는 결과를 찾는 열쇠입니다. 냉장고는 결과를 보관해 두는 창고입니다.

이 절은 빌드 캐시가 무엇을 믿고 서는지부터 봅니다. 그다음 결과를 찾는 열쇠를 어떻게 만드는지, 증분 빌드와 무엇이 다른지 봅니다. 마지막으로 캐시가 틀리는 때와 치르는 값을 봅니다.

같은 입력이면 같은 출력

빌드는 소스 코드를 실행할 수 있는 결과물로 바꾸는 일입니다. 빌드 도구는 이 일을 태스크라는 작은 단계로 나눠 돌립니다. 컴파일 한 번, 테스트 한 벌, 배포 묶음 만들기 한 번이 각각 태스크입니다.

태스크 하나는 입력을 받아 출력을 냅니다. 컴파일 태스크라면 소스 파일과 컴파일 옵션이 입력입니다. 컴파일러가 만들어 낸 클래스 파일이 출력입니다.

빌드 캐시는 태스크가 수학의 함수처럼 군다고 믿습니다. 입력이 같으면 몇 번을 돌려도 같은 출력이 나온다는 믿음입니다. 이 믿음이 맞으면 한 번 돌린 결과를 보관했다가 다음에 꺼내 써도 새로 돌린 것과 다르지 않습니다.

함수의 결과를 기억해 두었다가 같은 호출에 되돌려 주는 기법을 메모이제이션이라고 부릅니다. 같은 계산을 두 번 하지 않으려고 씁니다. 빌드 캐시는 이 생각을 함수 대신 빌드 단계에 건 것입니다.

입력을 값 하나로 줄인 캐시 키

보관해 둔 결과를 다시 찾으려면 열쇠가 있어야 합니다. 빌드 캐시에서는 이 열쇠를 캐시 키라고 부릅니다. 캐시 키가 같으면 보관된 결과를 꺼냅니다. 다르면 새로 만듭니다.

캐시 키는 태스크의 입력을 전부 모아 해시 함수에 넣어 만듭니다. 해시 함수는 아무리 긴 데이터도 짧은 고정 길이 값 하나로 줄여 주는 함수입니다. 입력이 한 글자만 달라져도 나오는 값이 크게 달라집니다. 그래서 파일 수백 개를 일일이 견주지 않고 값 하나만 견주면 됩니다.

키에 무엇을 넣느냐가 캐시가 맞는 결과를 내주는지를 가릅니다. 소스 파일만 넣으면 모자랍니다. 결과를 바꿀 수 있는 것은 전부 넣어야 합니다.

입력 왜 넣나
소스 파일의 내용 코드가 바뀌면 결과도 바뀝니다
컴파일 옵션 최적화 수준만 바꿔도 다른 결과물이 나옵니다
도구의 버전 컴파일러가 바뀌면 같은 소스에서 다른 결과가 나옵니다
기대는 라이브러리의 버전 라이브러리가 바뀌면 함께 묶이는 코드가 바뀝니다
앞 태스크의 출력 앞 태스크의 결과가 이 태스크의 입력이 됩니다

캐시 키는 파일의 수정 시각이 아니라 내용으로 만듭니다. 파일을 열었다 저장만 해도 수정 시각은 바뀝니다. 내용으로 키를 만들면 이런 일로 캐시가 빗나가지 않습니다.

적중과 미스

태스크 차례가 오면 빌드 도구는 먼저 캐시 키를 계산합니다. 그 키로 캐시를 찾아봅니다. 찾으면 캐시 적중, 못 찾으면 캐시 미스라고 부릅니다.

flowchart TD
    S["태스크 차례가 온다"] --> K["입력을 모아 캐시 키를 만든다"]
    K --> Q{"캐시에 이 키가 있나"}
    Q -- "있다 · 적중" --> H["보관된 출력을 꺼내 놓는다"]
    Q -- "없다 · 미스" --> R["태스크를 돌린다"]
    R --> W["출력을 키와 함께 캐시에 넣는다"]
    H --> N["다음 태스크로 넘어간다"]
    W --> N

미스가 난 태스크도 끝에 출력을 캐시에 남깁니다. 다음 빌드가 같은 키를 들고 오면 그때는 적중합니다.

적중은 흔히 연달아 일어납니다. 앞 태스크가 적중하면 그 출력은 지난번과 같습니다. 그 출력을 입력으로 받는 뒤 태스크도 키가 지난번과 같아져 또 적중합니다. 거꾸로 맨 앞에서 파일 하나가 바뀌면 그 출력에 기대는 뒤 태스크들이 대개 줄줄이 미스가 납니다.

증분 빌드와 가르는 선

빌드 캐시와 자주 헷갈리는 것이 증분 빌드입니다. 증분 빌드는 바뀐 곳만 다시 하는 빌드입니다. 둘 다 할 일을 건너뛴다는 점은 같습니다.

갈리는 것은 무엇과 견주느냐입니다. 증분 빌드는 바로 앞 빌드가 빌드 폴더에 남긴 결과하고만 견줍니다. 빌드 캐시는 보관해 둔 결과 전부와 견줍니다. 지난주 빌드든 다른 폴더의 빌드든 키만 같으면 꺼내 씁니다.

차이는 브랜치를 오갈 때 드러납니다. 브랜치 A 에서 빌드하고 B 로 옮겨 빌드한 뒤 다시 A 로 돌아왔다고 합시다.

상황 증분 빌드 빌드 캐시
B 에서 A 로 돌아온 직후 빌드 폴더에는 B 의 결과만 있어 바뀐 태스크를 다시 돌립니다 A 에서 만든 결과가 보관돼 있어 꺼내 씁니다
빌드 폴더를 지운 뒤 견줄 결과가 없어 전부 다시 합니다 캐시는 빌드 폴더 밖에 있어 꺼내 씁니다
처음 받은 기계 전부 다시 합니다 팀이 함께 쓰는 캐시가 있으면 꺼내 씁니다

빌드 폴더를 지우고 처음부터 도는 빌드를 클린 빌드라고 합니다. 캐시가 없으면 클린 빌드는 모든 태스크를 다시 돌립니다. 캐시가 있으면 폴더를 지워도 대부분의 태스크가 적중으로 끝납니다.

로컬 캐시와 원격 캐시

캐시를 어디에 두느냐로 두 갈래가 나뉩니다. 내 기계의 디스크에 두면 로컬 빌드 캐시입니다. 네트워크 너머 서버에 두고 여럿이 함께 쓰면 원격 빌드 캐시입니다.

로컬 캐시는 나 혼자 쓰는 창고입니다. 어제의 내가 만든 결과를 오늘의 내가 꺼내 씁니다. 네트워크를 타지 않으니 빠릅니다.

원격 캐시는 팀이 함께 쓰는 창고입니다. 동료가 만든 결과를 내가 받아 씁니다. 내가 만든 결과는 동료가 받아 씁니다. 캐시 키는 어느 기계에서 만들었는지와 상관없이 입력으로만 정해집니다. 그래서 누가 만든 결과든 섞어 쓸 수 있습니다.

원격 캐시는 흔히 지속적 통합 서버가 채웁니다. 지속적 통합은 코드를 올릴 때마다 서버가 빌드와 테스트를 자동으로 돌리는 방식입니다. 그 서버가 빌드하며 결과를 캐시에 올려 두면, 개발자는 같은 커밋을 받아 빌드할 때 대부분을 내려받기만 합니다.

sequenceDiagram
    participant 서버 as 지속적 통합 서버
    participant 캐시 as 원격 빌드 캐시
    participant 개발자 as 개발자 기계
    서버->>서버: 태스크를 돌린다
    서버->>캐시: 키와 출력을 올린다
    개발자->>캐시: 같은 키로 찾는다
    캐시-->>개발자: 출력을 내려 준다
    Note over 개발자: 태스크를 돌리지 않는다

원격 캐시는 흔히 쓰기를 지속적 통합 서버에만 열어 둡니다. 개발자 기계는 깔린 도구와 설정이 제각각이라 엉뚱한 결과를 올릴 수 있습니다. 원격 캐시에 한 번 잘못 들어간 결과는 같은 키를 찾는 모든 사람에게 퍼집니다. 이렇게 캐시에 틀린 결과가 섞여 퍼지는 일을 캐시 오염이라고 부릅니다.

캐시가 틀린 결과를 내는 때

빌드 캐시는 「같은 입력이면 같은 출력」이라는 믿음 위에 섭니다. 이 믿음이 깨지면 캐시가 틀립니다. 깨지는 방식은 둘입니다.

첫째는 키에서 입력이 빠질 때입니다. 태스크가 환경 변수를 몰래 읽는다고 합시다. 그런데 그 값은 키에 안 들어갔습니다. 값이 바뀌어도 키는 같으니 캐시는 낡은 결과를 적중이라며 내줍니다. 빌드는 성공합니다. 결과물만 틀려서 원인을 찾기 어렵습니다.

둘째는 출력이 매번 달라질 때입니다. 결과물 안에 빌드한 시각이나 기계 이름을 박아 넣으면 같은 입력에서도 출력이 달라집니다. 이런 앞 태스크가 미스가 나거나 다른 기계에서 다시 돌 때마다, 그 출력을 입력으로 받는 뒤 태스크는 소스가 그대로여도 키가 바뀌어 미스가 납니다. 틀린 결과는 안 나오지만 연달아 이어지던 적중이 거기서 끊깁니다.

같은 입력에서 늘 한 바이트도 다르지 않은 결과를 내는 빌드를 결정적 빌드라고 합니다. 빌드 캐시가 제값을 하려면 빌드가 결정적이어야 합니다.

캐시에서 빼는 태스크

모든 태스크를 캐시에 맡길 수는 없습니다. 파일을 내놓는 대신 바깥을 바꾸는 태스크가 그렇습니다. 서버에 배포하기, 저장소에 결과물 올리기, 데이터베이스 스키마 바꾸기가 그런 태스크입니다.

이런 변화를 부수 효과라고 부릅니다. 부수 효과는 결과를 돌려주는 것 말고 바깥에 남기는 변화입니다. 캐시가 적중해 이런 태스크를 건너뛰면 배포가 안 된 채로 빌드가 성공합니다. 그래서 부수 효과가 있는 태스크는 캐시 대상에서 뺍니다.

아주 가벼운 태스크도 빼곤 합니다. 캐시에서 꺼내는 일에도 시간이 듭니다. 파일 몇 개를 옮기는 태스크라면 키를 계산하고 결과를 받는 것보다 그냥 돌리는 쪽이 빠를 수 있습니다. 원격 캐시라면 더 그렇습니다.

도구마다 캐시하는 단위

빌드 캐시는 여러 빌드 도구가 갖추고 있습니다. 생각은 같습니다. 무엇을 한 단위로 캐시하느냐가 다릅니다.

도구 캐시하는 단위
Gradle 태스크 하나의 출력
Bazel 액션 하나의 출력. 액션은 명령 한 번을 돌리는 가장 작은 빌드 단계입니다
ccache C·C++ 소스 파일 하나를 컴파일한 결과
Docker 이미지의 레이어 하나. Dockerfile 에서 파일을 바꾸는 명령 한 줄이 레이어 하나를 만듭니다

Docker 는 캐시를 찾는 방식이 사슬처럼 이어져 있습니다. 레이어는 앞 레이어 위에 쌓이므로 한 줄에서 미스가 나면 그 아래 줄은 전부 다시 돌립니다. 그래서 자주 바뀌는 명령을 Dockerfile 아래쪽에 둡니다.

빌드 캐시의 대가

캐시는 공짜가 아닙니다. 건너뛴 시간만큼 다른 데서 값을 치릅니다.

대가 무엇이 드나
저장 공간 결과를 쌓을수록 디스크가 찹니다. 오래 안 쓴 결과부터 지우는 정리가 따라붙습니다
네트워크 원격 캐시는 결과를 올리고 받는 데 대역폭과 시간이 듭니다
키 계산 캐시를 찾기 전에 입력 파일을 전부 읽어 해시 값을 내야 합니다
틀린 적중 키가 입력을 놓치면 낡은 결과가 조용히 섞입니다

그래서 빌드 캐시가 언제나 이득인 것은 아닙니다. 태스크가 무겁고 같은 입력이 자주 되풀이될수록 이득이 큽니다. 모듈이 많은 큰 프로젝트나 매번 빈 기계에서 시작하는 지속적 통합 서버가 그런 경우입니다.

관련 항목

빌드 캐시가 속하는 상위 개념

캐싱 · 메모이제이션 · 빌드 · 빌드 최적화

빌드 캐시가 결과를 찾는 데 쓰는 수단

캐시 키 · 해시 함수 · 해시 · 체크섬 · 콘텐츠 주소 지정 · SHA-256

빌드 캐시가 건너뛰게 해 주는 빌드 단계

태스크 · 컴파일 · 테스트 · 패키징 · 레이어 · DAG · 의존성 그래프

빌드 캐시와 견주어지는 빌드 방식

증분 빌드 · 클린 빌드 · 병렬 빌드 · 분산 빌드

빌드 캐시가 놓이는 저장소

로컬 빌드 캐시 · 원격 빌드 캐시 · 아티팩트 저장소 · 객체 스토리지

빌드 캐시를 갖춘 빌드 도구

Gradle · Bazel · Docker · ccache · sccache · Turborepo · Nx · Buck2 · Pants

빌드 캐시가 겪는 캐시 현상

캐시 적중 · 캐시 미스 · 캐시 오염 · 캐시 무효화 · 낡은 데이터 · 축출

빌드 캐시가 기대는 성질

결정적 빌드 · 재현 가능한 빌드 · 순수 함수 · 멱등성 · 부수 효과 · 환경 변수

빌드 캐시를 쓰는 개발 환경

지속적 통합 · 모노레포 · GitHub Actions · Jenkins · 빌드 서버

다른 이름: build cache