Jib의 레이어 분리가 재빌드 비용을 줄이는 방식

목차

자바 클래스 하나를 바꿨는데 큰 의존성 묶음도 다시 전송해야 할까? 답은 빌드 도구가 파일을 어떻게 레이어로 묶고, 목적지 레지스트리에 그 레이어가 이미 있는지에 달려 있다. 빌드가 빨라지는 이유를 이미지가 작아졌다는 말로 설명하면 중요한 부분을 놓친다.

Jib 공식 FAQ에 따르면 기본적인 애플리케이션 레이어는 일반 의존성, SNAPSHOT 의존성, 프로젝트 의존성, 리소스, 클래스, 추가 디렉터리별 파일로 나뉜다. 실제로 어느 레이어가 달라지는지 보려고 독립적인 작은 Gradle 예제를 만들었다. JDK 21, Gradle 9.1.0, Jib 플러그인 3.5.3, digest로 고정한 Linux/arm64 Temurin 21 JRE를 사용했다. jibBuildTar로 로컬 tar만 세 번 만들고 레지스트리에는 올리지 않았다.

첫 빌드에는 Commons Lang 3.14.0과 baseline을 출력하는 클래스가 있다. 둘째에는 출력 문자열만 class-change로 바꾸고, 셋째에는 그 클래스를 그대로 둔 채 의존성을 3.15.0으로 올렸다. tar의 레이어 blob SHA-256 앞 12자리를 비교한 결과는 다음과 같다.

로컬 빌드일반 의존성 레이어클래스 레이어JVM 인자 파일 레이어tar 크기
첫 빌드8819277deda9aa6b0774bf26ab9629e924db113,591,296 B
클래스만 수정8819277deda99c5d0d9d4d5bab9629e924db113,591,296 B
의존성만 수정faefe63f79029c5d0d9d4d5b9db162f865b8113,603,584 B

앞의 공통 base image 여섯 레이어는 세 빌드에서 같았다. 마지막 변경에서는 의존성 JAR 이름이 클래스패스 파일에도 들어가므로 JVM 인자 파일 레이어까지 바뀌었다. 의존성 레이어만 바뀐다고 단정할 수 없는 실제 반례다. 다만 애플리케이션 클래스의 바이트는 같아 클래스 레이어 digest는 유지됐다.

레이어 digest는 JAR 파일 하나의 해시가 아니라 압축된 레이어 blob의 내용으로 결정된다. Jib은 파일 순서·시간·권한 등의 메타데이터를 일정하게 만들어 같은 입력이 같은 이미지 digest를 낼 수 있게 한다. 반대로 container.creationTime을 현재 시각으로 설정하거나 base image 태그가 다른 이미지를 가리키면 애플리케이션 소스가 같아도 최종 digest가 달라질 수 있다. Jib 재현성 설명

클래스 수정 뒤 실제로 재사용되는 것

둘째 빌드에서 의존성 blob은 그대로였고 클래스 blob만 달라졌다. 그 레이어를 가리키는 이미지 manifest도 바뀐다. 최종 이미지는 여전히 의존성 레이어와 클래스 레이어를 모두 참조한다. 실제로 첫 두 tar의 크기도 같았다. 따라서 작은 수정이 업로드 대상을 줄일 수는 있어도, 레이어 분리 자체가 최종 이미지 크기를 줄였다고 결론 낼 수 없다. tar 전체 크기는 포맷과 정렬의 영향도 받으므로 레지스트리 전송량으로 읽으면 안 된다. Jib의 레이어 목적, OCI 이미지 manifest

셋째 빌드에서는 의존성과 클래스패스 파일이 바뀌었다. 이 예제의 클래스는 의존성 API를 호출하지 않아 클래스 바이트가 그대로였지만, 새 API에 맞춰 코드를 수정하는 프로젝트라면 클래스 레이어도 바뀐다. SNAPSHOT을 별도 레이어에 두는 이유도 자주 바뀌는 의존성이 일반 의존성 전체의 재사용을 무너뜨리지 않도록 하기 위해서다. Jib의 기본 레이어 목록

캐시가 두 군데에 있다는 뜻

빌드 머신의 Jib 캐시는 변경되지 않은 레이어를 다시 구성하는 일을 줄인다. 레지스트리의 blob 저장소는 이미 가진 digest의 blob을 다시 업로드할 필요가 있는지를 가른다. OCI 배포 규격에는 레지스트리에 특정 digest가 있는지 묻는 HEAD /v2/<name>/blobs/<digest> 요청이 정의돼 있다. 로컬 캐시가 비어 있는 새 CI runner라도 목적지에 blob이 있다면 전송 조건은 다르고, 로컬 캐시가 따뜻해도 새 레지스트리에 그 blob이 없으면 업로드가 필요할 수 있다. OCI Distribution 규격

여기에 base image 조회가 따로 붙는다. Jib FAQ는 base image를 digest로 고정하지 않으면 최신 상태를 확인하려고 레지스트리에 다시 접근한다고 설명한다. 고정 digest는 입력의 정체성을 명확히 하지만, 오래된 런타임을 계속 쓰지 않도록 갱신 절차도 필요하다. Jib base image 캐시 설명

그래서 ‘빨라졌다’를 한 숫자로 보고하기 어렵다. 로컬 빌드 시간, 레지스트리에 전송한 바이트, 배포 노드가 내려받은 바이트, 최종 이미지 크기는 서로 다른 값이다. 어느 캐시가 따뜻했는지도 기록해야 비교가 된다.

같은 입력으로 레이어를 다시 만들어 본다

비교에 사용한 build.gradle은 아래와 같다. base image를 digest로 고정하고 생성 시각을 EPOCH로 두어, 이미지 태그 이동이나 현재 시각이 비교에 섞이지 않게 했다.

plugins {
    id 'java'
    id 'com.google.cloud.tools.jib' version '3.5.3'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.apache.commons:commons-lang3:3.14.0'
}

jib {
    from {
        image = 'eclipse-temurin:21-jre@sha256:d15c341db792d6bacbbe66d173f4ce278d4a7edf07336ab487e6a707c93c9fe4'
        platforms {
            platform {
                architecture = 'arm64'
                os = 'linux'
            }
        }
    }
    to {
        image = 'example.invalid/jib-layer-demo:local'
    }
    container {
        mainClass = 'example.Main'
        creationTime = 'EPOCH'
    }
}

settings.gradle에는 프로젝트 이름을 둔다.

rootProject.name = 'jib-layer-demo'

src/main/java/example/Main.java는 문자열 하나를 출력한다.

package example;

public final class Main {
    public static void main(String[] args) {
        System.out.println("baseline");
    }
}

클래스 변경과 의존성 변경을 분리한다

세 파일을 위 경로대로 저장한 뒤 프로젝트 디렉터리에서 실행한다.

gradle --no-daemon jibBuildTar
tar -xOf build/jib-image.tar manifest.json

첫 tar의 크기와 레이어 파일 이름을 기록한다. 다음에는 Main.java의 baseline을 class-change로 바꾸고 같은 명령을 실행한다. 마지막으로 build.gradle의 3.14.0만 3.15.0으로 바꿔 다시 비교한다. 한 번에 한 종류의 입력만 바꿔야 어떤 레이어가 그 변경을 담았는지 알 수 있다. 예제 파일 내려받기

각 단계에서 tar -xOf build/jib-image.tar manifest.json으로 레이어 파일 이름을 기록하고 다음 빌드와 비교한다. 특정 레이어의 역할은 tar -xOf build/jib-image.tar '레이어파일명.tar.gz' | tar -tzf -에서 app/libs, app/classes, 클래스패스 파일 경로를 확인하면 된다. 따옴표 안은 manifest에 나온 실제 파일명으로 바꿔 넣는다. 다음 빌드 전에 tar 크기도 기록한다. Jib Gradle 플러그인 문서

여기서 비교한 값은 레이어 blob과 tar 크기다. 레지스트리 업로드 바이트, 원격 캐시 hit, 배포 노드 다운로드, cold runner의 시간은 측정하지 않았다. 그 수치가 필요하면 고정한 이미지와 동일한 레지스트리를 대상으로 별도 실험해야 한다.