개발에 AtoZ까지

[개발도구] Spring Boot 이미지 만드는 세 가지 — layertools 쓰던 Dockerfile은 이제 안 돕니다 본문

개발도구

[개발도구] Spring Boot 이미지 만드는 세 가지 — layertools 쓰던 Dockerfile은 이제 안 돕니다

AtoZ 개발자 2026. 8. 29. 19:40
반응형

사내 Dockerfile을 열어 보면 이런 줄이 있을 수 있습니다.

RUN java -Djarmode=layertools -jar app.jar extract

이 명령은 지금 안 됩니다. Spring Boot 3.3에서 deprecated 됐고 3.5에서 제거됐습니다. spring-boot-jarmode-layertools 아티팩트도 3.2.12를 마지막으로 배포가 끊겼습니다.

그런데 한국어로 검색하면 아직 이 명령이 가장 많이 나옵니다. Spring Boot를 올리는 순간 빌드가 깨지는데, 원인을 찾기가 은근히 까다로운 종류입니다.

이 글을 다 읽으면 세 가지가 정리됩니다. 첫째, 현행 명령으로 Dockerfile을 고칠 수 있습니다. 둘째, Dockerfile·Buildpacks·Jib 중 우리 CI 환경에 맞는 걸 고를 수 있습니다. 셋째, 오래된 예제에서 베껴 쓰면 안 되는 지점 네 개를 피할 수 있습니다.

이 글은 Spring Boot 4.1.1 공식 문서와 릴리스 노트, Maven Central 배포 이력, Jib 저장소 문서를 확인해 정리했습니다(확인일 2026-08-23). 직접 여러 방식을 벤치마크한 결과가 아니라 문서 기준이라, 이미지 크기 같은 수치 비교는 넣지 않았습니다(공식 문서에도 비교 수치가 없습니다).

세 가지 경로와 갈림길 — Docker 데몬을 쓸 수 있는지가 먼저 결정됩니다

현행 명령으로 먼저 고치기

바뀐 것만 대조하면 이렇습니다.

옛 명령 (3.2 이하) 현행 명령 (3.3 이후)
-Djarmode=layertools -jar app.jar extract -Djarmode=tools -jar app.jar extract --layers --destination extracted
-Djarmode=layertools -jar app.jar list -Djarmode=tools -jar app.jar list-layers

jarmode=tools로 들어가면 쓸 수 있는 명령이 세 개입니다.

$ java -Djarmode=tools -jar my-app.jar

Available commands:
  extract       Extract the contents from the jar
  list-layers   List layers from the jar that can be extracted
  help          Help about any command

아티팩트 배포 이력으로도 확인됩니다. spring-boot-jarmode-layertools는 2.3.0.RELEASE부터 3.2.12까지 118개 버전이 나왔고 거기서 멈췄습니다. 후속인 spring-boot-jarmode-tools는 3.3.0부터 시작합니다.

그리고 현행 문서에는 layertools라는 문자열이 아예 없습니다. 3.3·3.4·3.5·4.1.1의 Dockerfiles 문서와 Maven·Gradle 플러그인 OCI 문서를 전부 훑어도 0건입니다.

공식 Dockerfile 예시가 이렇게 바뀌었습니다

4.1.1 문서의 멀티스테이지 Dockerfile입니다. 그대로 옮기면 이렇습니다.

FROM bellsoft/liberica-openjre-debian:25-cds AS builder
WORKDIR /builder
COPY build/libs/*.jar application.jar
# Extract the jar file using an efficient layout
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted

FROM bellsoft/liberica-openjre-debian:25-cds
WORKDIR /application
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
ENTRYPOINT ["java", "-jar", "application.jar"]

여기서 놓치기 쉬운 게 ENTRYPOINT입니다. 예전 예제들은 JarLauncher를 직접 호출했습니다.

# 옛 예제 — 두 군데가 낡았습니다
ENTRYPOINT ["java", "-cp", "app:app/lib/*", "org.springframework.boot.loader.JarLauncher"]

두 가지 문제가 있습니다. 첫째, 클래스 패키지가 이동했습니다. 3.2에서 org.springframework.boot.loader.JarLauncherorg.springframework.boot.loader.launch.JarLauncher로 옮겨졌습니다. 둘째, 현행 문서는 JarLauncher를 아예 호출하지 않고 java -jar application.jar를 씁니다.

문서가 그 이유를 이렇게 씁니다.

"This jar only contains application code and references to the extracted jar files. This layout is efficient to start up and AOT cache (and CDS) friendly."

여기서 application.jar는 uber jar가 아닙니다. 애플리케이션 코드와 추출된 jar 참조만 담긴 jar입니다. 이 레이아웃이 AOT 캐시와 CDS에 친화적이라는 게 문서의 설명입니다.

레이어를 왜 네 개로 나누나요

extract --layers가 만드는 디렉터리 순서가 중요합니다. 변경 빈도가 낮은 것부터 COPY합니다.

순서 레이어 얼마나 자주 바뀌나
1 dependencies 거의 안 바뀜
2 spring-boot-loader 거의 안 바뀜
3 snapshot-dependencies 가끔
4 application 매 커밋

이렇게 쌓으면 코드만 바뀐 배포에서 위 세 레이어가 캐시로 재사용됩니다. 다만 문서는 "often"이라고 씁니다. 항상 맨 아래 레이어만 바뀐다고 단정하면 과장입니다.

반대로 uber jar를 그대로 COPY해서 java -jar app.jar로 실행하면 두 가지 손해가 있다고 문서가 지적합니다. 압축 해제 없이 실행할 때의 오버헤드코드와 의존성이 한 레이어에 묶이는 것입니다.

여기서도 정확하게 쓰는 게 좋습니다. 문서는 오버헤드를 "a certain amount of overhead", "can be noticeable"이라고만 하고 몇 초나 몇 퍼센트 같은 수치는 제시하지 않습니다.

참고로 unzipmv 조합으로도 같은 일을 할 수 있습니다. 다만 문서가 jarmode가 그 일을 단순화하고, jarmode가 만드는 레이아웃은 별도 작업 없이 AOT 캐시 친화적이라고 밝힙니다.

Spring Boot 공식 Dockerfile 예시 — jarmode=tools와 ENTRYPOINT (확인일 2026-08-23)

Buildpacks: Dockerfile을 안 쓰는 길

Dockerfile을 아무도 관리하고 싶지 않다면 이 경로가 있습니다.

# Maven
mvn spring-boot:build-image

# Gradle
gradle bootBuildImage

Spring Boot 플러그인이 Cloud Native Buildpacks로 이미지를 만들어 줍니다. Dockerfile이 필요 없습니다.

기본 빌더가 3.5에서 바뀌었습니다. 예전 문서를 보고 paketobuildpacks/builder:basebuilder-jammy-base를 기대하면 안 맞습니다.

현행 기본 빌더: paketobuildpacks/builder-noble-java-tiny:latest

Ubuntu 24.04 Noble Numbat 기반이고, 셸이 없습니다. 시스템 라이브러리도 축소돼 있습니다. 그래서 docker exec ... /bin/sh로 들어가 확인하던 진단 절차나 start script를 전제한 설명이 그대로 통하지 않습니다. 셸이 필요하면 run image를 paketobuildpacks/ubuntu-noble-run:latest로 교체하면 됩니다.

자바 버전은 대개 신경 쓰지 않아도 됩니다. Spring Boot 플러그인이 프로젝트의 target 호환성(Maven은 compiler 플러그인 설정이나 maven.compiler.target, Gradle은 targetCompatibility)을 감지해 buildpacks에 같은 자바 버전을 설치하도록 지시합니다. BP_JVM_VERSION은 그 자동 동작을 덮어쓰는 override입니다.

// 명시하고 싶을 때
tasks.named("bootBuildImage") {
    environment.put("BP_JVM_VERSION", "25")
}

다만 Paketo 쪽 문서와 실제 buildpack 선언이 서로 다릅니다. paketo.io 문서는 기본값을 '릴리스 시점 최신 17.x'로, bellsoft-liberica buildpack.toml(main)은 "21"로 적고 있습니다. 그러니 자바 버전이 중요한 프로젝트라면 명시하는 편이 안전합니다.

캐시 효율에 관한 팁이 하나 있습니다. buildCache와 launchCache 볼륨 이름은 기본적으로 이미지 전체 이름에서 파생됩니다. 태그에 버전을 넣으면 배포마다 캐시가 무효화됩니다. 볼륨 이름을 고정해 두시면 좋습니다.

결정적 제약bootBuildImagespring-boot:build-image는 둘 다 문서가 "requires access to a Docker daemon"이라고 못박습니다. CI 러너에 데몬을 못 띄우는 환경이면 이 경로는 막힙니다.

Jib: Docker 데몬이 없을 때

여기서 Jib이 들어옵니다. Jib README의 첫 문장이 "without a Docker daemon"이고, 설계 목표 중 하나가 'Daemonless'입니다.

# 레지스트리로 직접 푸시 — 데몬 불필요
mvn jib:build

# 로컬 데몬에 올리기 — 이때만 docker 명령 필요
mvn jib:dockerBuild

현재 버전은 jib-maven-plugin 3.5.2, jib-gradle-plugin 3.5.4(2026년 7월 14일)입니다.

그런데 Spring Boot와 함께 쓸 때 주의할 점이 있습니다. Jib FAQ가 팻 jar 컨테이너화를 권하지 않는다고 직접 씁니다("highly recommend against"). Spring Boot 팻 jar의 내장 의존성이 dependencies 레이어로 가지 않고 classes/resources 레이어로 들어가기 때문입니다.

Spring Boot 팻 jar가 layers.idx를 존중하는 건 jib CLI의 jar 명령 exploded 모드에서만입니다.

그리고 Jib FAQ의 엔트리포인트 예시는 아직 3.2 이전 패키지(org.springframework.boot.loader.JarLauncher)로 적혀 있습니다. Spring Boot 3.2 이상에서 그대로 베껴 쓰면 안 됩니다.

한 가지 더 짚어 두면, Spring Boot 공식 문서는 컨테이너 이미지 경로로 Dockerfile과 Buildpacks 두 가지만 규정합니다. Jib은 Spring Boot 문서에 안 나오고, jib-maven-plugin README에도 Spring Boot 언급이 없습니다. 그래서 Jib은 "Spring Boot 표준 경로"가 아니라 "데몬 없는 환경의 대안"으로 보는 게 정확합니다.

Jib 저장소의 daemonless 설명 (확인일 2026-08-23)

네이티브 이미지는 언제 보나요

이미지 크기가 최우선이고 시작 시간도 줄여야 한다면 검토 대상입니다. JVM을 포함하지 않아 이미지가 더 작다는 것이 문서 근거입니다.

다만 자바 버전 제약이 셉니다. 4.1.1 문서가 이렇게 씁니다. Buildpacks가 컴파일에 쓴 자바 버전과 동일한 GraalVM native-image 버전을 사용하기 때문에, 애플리케이션을 최소 JDK 25로 빌드해야 합니다.

지원 버전 표도 GraalVM Community 25 / Native Build Tools 1.1.8입니다(이 버전을 '1.8'로 줄여 쓰면 틀립니다).

macOS에서 빌드한다면 Docker 메모리를 8GB 이상으로 올리라고 문서가 권고합니다.

Java 17~21로 고정된 서비스라면 네이티브 이미지는 아직 대기입니다.

AOT 캐시를 쓰는 Dockerfile(-XX:AOTCacheOutput / -XX:AOTCache)도 Java 25 이상 전용입니다. Java 24에서는 CDS 변형(-XX:ArchiveClassesAtExit / -XX:SharedArchiveFile)을 쓰되, 문서는 24 이상이면 CDS 대신 AOT 캐시를 권합니다.

그래서 뭘 고르나요

민수 씨 팀 상황으로 정리하겠습니다. Spring Boot 3.5에서 4.1로 올리는 중이고, GitHub Actions에서 Docker 데몬을 쓸 수 있고, Dockerfile은 3.1 시절에 만든 걸 그대로 쓰고 있습니다.

이 팀은 먼저 layertoolstools ... extract --layers로 바꿔야 합니다. 그리고 ENTRYPOINT에 JarLauncher가 하드코딩돼 있으면 java -jar application.jar로 바꿉니다. 그 뒤에 Buildpacks로 옮길지는 별도 결정입니다.

일반화하면 이렇습니다.

Dockerfile + layered jar를 고를 경우

  • Docker 데몬을 쓸 수 있고, 베이스 이미지와 JVM 옵션을 직접 통제하고 싶다
  • 사내 보안 정책상 베이스 이미지를 지정해야 한다
  • 공식 예시를 그대로 쓰면 4줄 COPY로 끝납니다

Buildpacks(bootBuildImage)를 고를 경우

  • Dockerfile을 아무도 관리하고 싶지 않다
  • 로컬과 CI에 Docker 데몬이 있다
  • non-root 실행 같은 기본값을 그대로 받고 싶다

Jib을 고를 경우

  • CI 러너에 Docker 데몬을 띄울 수 없다(데몬리스 쿠버네티스 러너 등)
  • 레지스트리로 직접 푸시하면 된다

하지 말아야 할 것

  • uber jar를 그대로 COPY해서 단일 레이어 이미지 만들기
  • 새 파이프라인에 -Djarmode=layertools 쓰기
  • org.springframework.boot.loader.JarLauncher(launch 없는 옛 패키지)를 ENTRYPOINT에 하드코딩하기
  • 기본 빌더 이미지에 셸이 있다고 전제한 진단 절차 남겨 두기
  • Docker 데몬 없는 CI에서 bootBuildImage 시도하기
  • Jib에 Spring Boot 팻 jar를 packaged 모드로 넣기
  • 방식별 이미지 크기를 MB 수치로 단정하기 — 공식 근거가 없습니다

대기할 것

  • Java 17~21 고정 서비스의 네이티브 이미지 — 최소 JDK 25가 필요합니다
  • Jib을 Spring Boot 표준 경로로 삼는 결정 — 공식 문서에 없습니다

마이그레이션 확인 목록

[ ] Dockerfile 에 -Djarmode=layertools 가 남아 있는지 검색했다
[ ] ENTRYPOINT 에 JarLauncher 하드코딩이 있는지 확인했다
[ ] JarLauncher 를 쓴다면 org.springframework.boot.loader.launch 패키지인지 확인했다
[ ] COPY 순서가 dependencies → spring-boot-loader → snapshot-dependencies → application 인가
[ ] bootBuildImage 를 쓴다면 기본 빌더가 builder-noble-java-tiny 로 바뀐 걸 알고 있다
[ ] 셸이 필요한 진단 절차가 있으면 run image 를 교체했다
[ ] CI 에 Docker 데몬이 있는지 확인했다
[ ] BP_JVM_VERSION 을 명시했다 (자바 버전이 중요한 경우)
[ ] buildCache/launchCache 볼륨 이름을 고정했다

찾을 대상을 먼저 훑으면 작업량이 보입니다.

# 폐기된 명령 사용처
grep -rn "jarmode=layertools" . --include=Dockerfile --include=*.yml --include=*.yaml --include=*.sh

# 옛 패키지의 JarLauncher
grep -rn "org.springframework.boot.loader.JarLauncher" . --include=Dockerfile --include=*.yml

# 옛 기본 빌더를 명시해 둔 곳
grep -rn "builder-jammy\|paketobuildpacks/builder:" . --include=*.gradle --include=*.kts --include=pom.xml

# 현행 명령 확인 (로컬에서 바로 실행 가능)
java -Djarmode=tools -jar build/libs/app.jar list-layers

다음에 볼 글

컨테이너로 띄우다 명령 자체가 안 먹는 상황이라면 docker-compose 명령과 Compose V2 전환이 먼저 볼 글입니다. 예전 하이픈 명령과 version 키가 사라진 게 원인인 경우가 많습니다. docker-compose up 에러에는 실제 에러 메시지별 대응을 정리해 뒀습니다. Spring Boot 버전 자체를 올리는 판단은 이번 주에 올린 Spring Boot 3 → 4 업그레이드 글에서 다뤘고, 그 글의 베이스라인 표(Gradle 8.14 이상 등)가 이미지 빌드에도 그대로 걸립니다.

Spring Boot는 기본 빌더 이미지나 jarmode 명령처럼 도구 계층이 마이너 버전마다 바뀝니다. 이 글의 내용은 2026년 8월 23일에 4.1.1 문서 기준으로 확인한 것이니, 본인 대상 버전의 Container Images 문서로 한 번 더 확인해 주세요. 잘못된 내용이나 보충이 필요한 부분이 있으면 댓글로 알려 주시면 반영하겠습니다.

참고한 자료

  • Spring Boot 4.1.1 Reference — Container Images(Efficient Container Images, Dockerfiles) (확인일 2026-08-23)
  • Spring Boot Maven Plugin / Gradle Plugin — Packaging OCI Images, 기본 빌더와 캐시 설정 (확인일 2026-08-23)
  • Spring Boot 3.3 Release Notes — jarmode layertools deprecated 안내 (확인일 2026-08-23)
  • Spring Boot 3.5 Release Notes — 3.3 deprecated 항목 제거 (확인일 2026-08-23)
  • Maven Central — spring-boot-jarmode-layertools / spring-boot-jarmode-tools 배포 이력 (확인일 2026-08-23)
  • Spring Boot 4.1.1 Reference — System Requirements, GraalVM·Native Build Tools 버전 (확인일 2026-08-23)
  • Spring Boot How-to — Developing Your First Native Application (확인일 2026-08-23)
  • GoogleContainerTools/jib — README와 FAQ (확인일 2026-08-23)
반응형
Comments