| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
- 중간 장소 찾기
- kusitms
- 중간 장소 추천
- 한국대학생it경영학회
- 약속 장소 추천
- springboot
- JPQL
- Container Registry
- K3S
- 불변객체
- 모이삼
- ddd
- 자바 ORM 표준 JPA 프로그래밍
- 모임 장소 추천
- 중간 지점 찾기
- 이펙티브자바
- Spring
- 객체지향 쿼리 언어
- cicd
- 최범균
- Domain Driven Design
- 도메인 주도 개발 시작하기
- java
- Docker cache
- 중간 지점 추천
- JPA
- GitHub Actions
- 쿠버네티스
- 큐시즘
- Docker Layer
- Today
- Total
코딩은 마라톤
Jar를 다이어트 시켜보자 (Layered Jar) 본문
요새 AWS로 마이그레이션을 진행하고 있다.
마이그레이션을 진행하기 앞서, 배포 과정에서의 문제점을 살펴보고 개선할 수 있는 부분은 개선하고자 했다.

서버 로그를 살피고 있던 도중, 어느샌가 스프링 애플리케이션 초기 실행 시간이 40초에 근접했다.
사실 나는 Github Actions가 돌아가는 것이 배포의 시작이자 끝이라 생각했다.
하지만 애플리케이션 초기 실행 시간, 그리고 배포된 Docker Image를 Pull 하는 과정에 대해선 간과하고 있었다.
- 초기 실행 시간을 줄일 순 없을까?
- 배포 과정에서 도커 이미지를 만들고 push, pull 과정에서 개선할 수 없을까?
그래서 나는 위 두 가지의 고민을 해결하고자 찾던 도중 "Layered Jar"를 알게 되었다.
Layered Jar
흔히 `./gradlew bulid`를 통해 만들어진 Jar은 "Fat Jar" 라고 한다.

사진에서 보이는 server-0.0.1-SNAPSHOT.jar 내부에는 사용된 모든 라이브러리와 내가 짠 코드가 몽땅 들어있다.
그래서 보통은 배포할 때 `java -jar app.jar` 명령 한 줄로 어디서든 실행할 수 있다.
다만 소스 코드 한 줄만 수정해도 100MB가 넘는 Jar 파일이 새롭게 만들어진다.
"Layered Jar" 은 도커 이미지 생성 개선을 위해 Spring Boot 2.3.0에 도입된 것으로, Jar 내부에 'layers.idx'라는 인덱스 파일이 존재한다.
'layers.idx'는 의미 그대로 Jar 내부 파일이 어떤 레이어에 속하는지를 정의하는 지도 역할을 한다.
Spring Boot 는 다음 4가지 레이어를 기본으로 제공한다.

- dependencies: 버전에 SNAPSHOT이 포함되지 않은 일반 외부 라이브러리. (가장 안 변함)
- spring-boot-loader: Jar를 실행하기 위한 로더 클래스들.
- snapshot-dependencies: 버전에 SNAPSHOT이 포함된, 자주 바뀔 수 있는 내부 라이브러리.
- application: 우리가 작성한 소스 코드(. class)와 리소스 파일들. (가장 자주 변함)
Fat Jar, Layered Jar를 비교하는 것만으론 Layered Jar의 장점을 이해하기 어렵다.
왜냐하면 Layered Jar는 Dockerfile로 빌드할 수 있는 최적화된 Docker 이미지를 더 쉽게 생성하기 위해 나온 것이기 때문이다.
🐳 Docker 의 이미지 구축 방식

도커 이미지는 여러 개의 레이어를 가진다.
사진처럼 DockerFile에 작성된 명령어마다 레이어를 가지며 순서대로 차곡차곡 쌓인다.
또한 새로운 이미지가 만들어졌을 때의 레이어의 변경점이 없다면, 새 이미지의 레이어를 덮어 씌우는 것이 아닌 레이어 캐시를 사용해 레이어를 재사용한다. 레이어의 재사용은 빌드 속도를 향상할 수 있다.
따라서 Layered Jar의 4가지 레이어를 도커 레이어에 각각 배치한다. 변경될 가능성이 거의 없는 레이어(dependencies, spring-boot-loader ..)들은 도커 캐시를 통해 재사용하고, 매번 수정되는 application 레이어만 교체하는 식으로 개선할 수 있다.
결과적으로 Fat Jar 방식처럼 큰 덩어리를 전부 바꿔치기 하는 대신, 코드가 변경된 일부만 쏙 골라 배포할 수 있게 된다.
Layered Jar를 사용해서 개선해보자
FROM eclipse-temurin:21-jre-alpine AS builder
WORKDIR /app
ARG JAR_FILE=./*.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
멀티 스테이지로 DockerFile을 구성하여 builder 스테이지에서 추출한 레이어를 runner 스테이지에서 COPY 하여 사용하도록 했다.
- builder 스테이지: `java -Djarmode=layertools -jar` 명령어로 JAR에서 레이어를 추출(extract)하는 과정.
- runner 스테이지: 추출된 4개의 레이어를 순서대로 COPY 하는 과정.
❗️COPY 과정이 매우 중요❗️
도커 레이어는 특정 레이어에서 변경 사항이 생길 경우, 이후 모든 레이어를 전부 무효화한다.
따라서 COPY 시, 변경 사항이 가장 낮은 dependencies 순으로 진행해야 한다.
❗️Spring Boot 3.2 이상 사용 중이라면?❗️
RUN java -Djarmode=layertools -jar app.jar extract 이 Deprecated 되었다고 한다.
따라서 다음과 같이 변경한다.
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher
(바꾸지 않아도 동작하지만,, 혹시 모르니 변경하기를 바란다.)
이렇게 구성한 뒤, 두 번째 배포부터 레이어의 캐싱을 확인할 수 있다.



개선 결과

- 좌측 그래프: 이미지 Pull 성능 비교 (PULL 할 경우)
- 내용: 쿠버네티스 노드가 이미지를 다운로드하는 데 걸린 시간
- 해석: 기존 Jar 방식은 전체를 다운로드해야 해서 4초가 걸렸지만, Layered Jar 방식은 변경된 소스(application) 레이어만 다운로드하므로 1초 만에 완료되었다. 현재는 도커 이미지가 크지 않아 엄청 크게 와닿지는 않지만, 도커 이미지가 커진다면 75%의 시간 단축은 꽤나 유의미한 개선일 것 같다.
- 우측 그래프: 초기 실행 성능 비교 (초기 실행 시간)
- 내용: 스프링 애플리케이션이 시작되어 서비스 준비가 되기까지 걸린 시간
- 해석: 기존 Jar 방식의 40초에 비해, Layered Jar 방식은 20초로 단축되었다. 기존 Jar 방식은 압축이 되어 있어 압축 해제하고 클래스 로딩하는데 시간이 소요되지만, Layered Jar 방식은 DockerFile에서 extract로 압축을 풀었기 때문에 곧장 클래스 로딩을 하여 시간이 단축된 것으로 확인된다.
결론
Layered Jar 적용 후, Github Actions 빌드가 끝난 시점부터 실제 서비스가 가동되기까지의 소요 시간을 44초에서 21초로 약 52% 단축했다. 무중단 배포가 되어 있지 않을 경우, Layered Jar을 적용하면 다운타임을 줄일 수 있으므로 용이할 것 같다. 다음에는 애플리케이션 클래스 데이터 공유(Application Class-Data Sharing)를 활용해서 시간을 더 낮출 수 있을지도 확인해 봐야겠다. Spring AOT라는 것도 있던데.. 이것도 되면 확인해 봐야겠다.
출처
- https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1
- https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3
- https://www.geeksforgeeks.org/devops/what-is-docker-image-layer/
- https://netmarble.engineering/class-data-sharing-cds-and-layered-jar/
'Backend > CI CD' 카테고리의 다른 글
| [Kubernetes] K3s로 운영·스테이징 환경을 단일 서버에서 분리하기 (2) (2) | 2025.07.24 |
|---|---|
| [Kubernetes] K3s로 운영·스테이징 환경을 단일 서버에서 분리하기 (1) (0) | 2025.07.22 |
| [FAIL] Github Actions을 이용해 Docker Cache 관리하고 싶었으나,, (2) | 2024.04.21 |
| Github Actions, Docker, EC2를 이용한 CI/CD 구현하기 (4) | 2024.04.07 |