jdeps와 jlink를 활용한 Java 기반 컨테이너 경량화 본문
Overview
컨테이너로 배포되는 애플리케이션은 컨테이너 이미지의 크기가 작을수록 빠르게 실행하고 확장할 수 있으며 이미지 보관 및 전송에 드는 비용이 절감된다.
Java 기반의 애플리케이션은 JVM이 함께 배포되어야 하기 때문에 Go 언어와 같은 바이너리 형태로 배포되는 애플리케이션에 비해 컨테이너 이미지의 크기가 비대하다.
이번 글에서는 jdeps
와 jlink
가 각각 어떤 명령어인지 알아보고 해당 명령어를 통해 Java 기반의 애플리케이션 컨테이너를 경량화 하는 방법에 대해 알아보고자 한다.
본 포스팅은 AWS 기술 블로그와 일본 개발자분의 포스팅을 토대로 작성되었다.
https://qiita.com/yTakada-gxp/items/f681d28f31e999e5dfae
https://aws.amazon.com/ko/blogs/tech/amazon-corretto-base-container-diet/
jdeps
https://docs.oracle.com/en/java/javase/11/tools/jdeps.html#GUID-A543FEBE-908A-49BF-996C-39499367ADB4jdeps
명령어는 .class 파일의 패키지 혹은 클래스 레벨의 의존성을 보여준다. 모듈화가 적용된 jdk9
버전 이후에만 동작하기 때문에 버전 지정시 주의해야 한다. jdeps
에는 여러가지 옵션이 존재하지만, 이번 글에서 주요하게 사용하게 되는 옵션은 다음과 같다.
--ignore-missing-deps
: 결락되어 있는 의존성을 무시한다. 즉, 의존성을 알 수 없는 모듈은 제외--print-module-deps
: 모듈 의존성을 콤마로 구분된 리스트 형태로 출력한다.--class-path <path>
: 클래스 파일의 위치를 지정한다.
위에 설명한 옵션들을 사용해서 의존성이 존재하는 모듈을 추출하는 코드는 다음과 같다.
$ jdeps \
--ignore-missing-deps \
--print-module-deps \
-q \
-R \ ##모든 런타임의 종속성을 재귀적으로 탐색한다
--multi-release 17 \ ##의존성을 분석할 버전을 지정한다.
--class-path="./<app_name>/BOOT-INF/lib/*" \
--module-path="./<app_name>/BOOT-INF/lib/*" \
build/libs/<jar_file_name>.jar
SpringBoot를 사용하는 경우 jar
파일 내부에 /BOOT-INF/lib/
라는 디렉토리에 의존 라이브러리를 보유하고, 실행될 때 읽어오는 구조를 가지고 있다. 해당 jar
을 jdeps
의 클래스패스에 지정할 필요가 있다.
jlink
https://docs.oracle.com/en/java/javase/11/tools/jlink.htmljlink
명령어를 통해 모듈과 모듈의 의존성을 커스텀 런타임 이미지에 결집하고 최적화 할 수 있다. jlink
의 여러 옵션들 중 이번 글에서 주요 사용되는 옵션은 다음과 같다.
--add-modules [...]
: 모듈과 모드를 기본 루트 모듈에 추가한다. 기본 루트 모듈은 비어있다.--strip-debug
: 출력에서 디버그 정보를 제거한다.--no-man-page
: man pages
를 제거한다.--no-header-files
: header files
를 제거한다.--compress=2
: 리소스의 압축을 가능하게 한다.
0
: 압축 불가
1
: 상수 문자열 공유
2
: ZIP--output path
: 런타임 이미지를 생성할 위치를 지정한다.
$ jlink \ --verbose \. ##상세한 추적을 활성화 하여 로깅합니다.
--add-modules [] \ ## jdeps 명령어를 통해 확인한 의존성을 입력한다.
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output <path>
jdeps
와 jlink
를 활용하여 만들어진 jre
파일은 해당 자바 애플리케이션을 실행하기 위한 최소 모듈만을 포함하고 있으며 최소한의 CLI만 포함된 상태로 생성된다.
Write Dockerfile
위에서 작성한 jdeps
와 jlink
명령어를 도커파일에 적용시켜 경량화된 컨테이너 이미지를 생성해보는 실습을 진행해보자.
해당 Dockerfile은 애플리케이션이 이미 jar
파일로 빌드된 상태임을 가정하고 작성되었다. 또한, 클라우드 플랫폼 중립적으로 작성하기 위해 amazon-correctto
가 아닌 eclipse-temurin
을 기반으로 작성했다.
# Stage 1
FROM eclipse-temurin:<version>-alpine as deps
COPY ./<jar_file_name>.jar /app/<jar_file_name>.jar
RUN mkdir /app/unpacked && \
cd /app/unpacked && \
unzip ../<jar_file_name>.jar && \
cd .. && \
$JAVA_HOME/bin/jdeps \
--ignore-missing-deps \
--print-module-deps \
-q \
--recursive \
--multi-release <version> \
--class-path="./unpacked/BOOT-INF/lib/*" \
--module-path="./unpacked/BOOT-INF/lib/*" \
./<jar_file_name>.jar > /deps.info
# Stage 2
FROM eclipse-temurin:<version>-alpine as temurin-jdk
RUN apk add --no-cache binutils
COPY --from=deps /apo/deps.info /deps.info
RUN $JAVA_HOME/bin/jlink \
--verbose \
--add-modules $(cat /deps.info) \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /<custom_jre_name>
# Stage 3
FROM alpine:<version>
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=temurin-jdk /<custom_jre_name> $JAVA_HOME
ARG APPLICATION_USER=appuser
RUN adduser --no-create-home -u 1000 -D $APPLICATION_USER
RUN mkdir /app && \
chown -R $APPLICATION_USER /app
USER 1000
COPY --chown=1000:1000 ./<jar_file_name>.jar /app/<jar_file_name>.jar
WORKDIR /app
EXPOSE 8080
ENTRYPOINT [ "/jre/bin/java", "-jar", "/app/<jar_file_name>.jar" ]
Stage 1
은 temurin:<version>-alpine
이미지를 베이스 이미지로 사용하여 jdeps
를 이용한 의존성 분석 및 분석 결과를 deps.info
라는 이름으로 생성한다.
Stage 2
는 Stage 1
과 동일한 이미지와 생성된 의존성 목록을 바탕으로 jlink
를 사용해 경량화된 커스텀 jre
를 생성한다.
Stage 3
는 alpine:<version>
을 베이스로 Stage 2
에서 생성된 커스텀 jre
를 사용하여 최종 이미지를 생성한다. 이 과정에서 보안을 위해 별도의 유저를 생성하여 권한을 부여하고 jar
파일을 실행한다.
여기까지 작성한 Dockerfile
을 docker build -t <image_name>:<version> .
명령을 통해 이미지로 빌드시킨다. Dockerfile
설정을 잘못 적지 않았다면 성공적으로 확연하게 줄어든 43MB
용량의 이미지가 생성된 것을 확인할 수 있다.

비교를 위해 alpine jdk
만을 써서 빌드한 이미지의 용량과 비교해보자. 아래는 비교를 위해 작성한 간단한 Dockerfile
이다.
FROM eclipse-temurin:17-alpine
CMD ["./gradlew", "clean", "build"]
VOLUME /tmp
ARG JAR_FILE=./app.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
jdeps
와 jlink
를 사용하지 않았기 때문에 의존성이 최적화되지 않아 불필요한 의존성까지 모두 포함된 이미지가 생성된다.

리사이징된 이미지와 비교했을 때 이미지의 용량이 약 80~90% 감소됐다.

실행 또한 성공적으로 되는 것을 확인할 수 있다.

'DevOps' 카테고리의 다른 글
Observability (O11y) (0) | 2025.03.10 |
---|---|
top 명령어 제대로 알고 쓰자 (0) | 2025.02.04 |
[Terraform] 테라폼 기초 (3) (1) | 2024.08.28 |
[Terraform] 테라폼 기초 (2) (0) | 2024.08.19 |
[Helm] Helm Quick Start (0) | 2024.08.10 |