현 회사에서 개발 중인 프로젝트는 지금 기준으로는 Maven 프로젝트로 되어있고 스프링부트 버전도 2.3.x이며 각종 라이브러리의 버전도 낮은 상태로 레거시라고 할만한 프로젝트였다.
그래서 마이그레이션도 할 겸, 멀티 모듈로 넘어갈 준비도 할 겸 개발 서버와 릴리즈 서버에 배포 소요 시간을 줄이기 위해 Gradle로 빌드툴 변경을 하였다. 이번에 마이그레이션을 하면서 공부한 내용 몇 가지를 기록해보겠다.
gradle은 처음 공부를 시작할때, 그리고 여러 토이 프로젝트를 할 때 그냥 막연히 가독성 좋고, 빠르고, 익숙하다고 자주 사용했었는데 무슨 이유에서 그런 것인지에 대해서는 자세히 알아본 적이 없었다. 덕분에 이번 기회에 gradle이 빠른 이유를 조금 알 수 있었다.
우선 Gradle의 장점에 대해 알고가보자.
[우아한테크 - 10분 테코톡] 루나의 Gradle
여기서 가장 체감이 많이되고 눈 여겨 볼 점은 빌드 속도가 빠르다는 것이다.
Gradle이 빌드 속도가 빠른 이유는 무엇때문일까?
데몬 프로세스와 점진적 빌드의 차이점은 데몬 프로세스는 gradle을 빌드 후 다음 빌드까지 메모리에 기억해두는 것이라고 이해하면 된다. 빌드 캐싱을 효과적으로 사용하기 위해 도움이 되는 프로세스이다.
Maven에서 Gradle 변환시 속도 체감을 해보자면
↓ maven 빌드 시간
↓ gradle 빌드 시간
gradle bootJar
: 단순히 프로젝트의 jar 파일만 생성gradle build
: bootJar, test를 포함한 build 관련 테스크를 모두 시행하면서 빌드gradle assemble
: 프로젝트의 결과물을 내는 모든 작업을 단일 작업으로 만드는 것gradle clean build
: 이전 빌드 파일을 지우고 빌드gradle clean build -x test
: 테스트 없이 이전 빌드 파일을 지우고 빌드이런 식으로 원하는 대로 옵션을 주면서 빌드할 수 있다.
그냥 gradle build
를 사용하면 로컬환경에서 빌드할 경우 로컬 환경에 설치된 java와 gradle 버전에 영향을 받게 된다.
반면 gradlew
는 gradle wrapper(내장 gradle)로 새로운 환경에서 프로젝트를 설정할 때 java나 gradle을 설치하지 않고 바로 빌드할 수 있게 해주는 역할을 한다.
gradle init
후 gradle wrapper
로 생성을 할 수 있는데 보통 이 방법은 거의 사용하지 않고 start.spring.io에서 Spring Boot 프로젝트를 생성해 사용하기 때문에 이미 gradlew가 생성되어 있어서 명령어로 직접 생성할 일이 거의 없다.
이 gradlew와 gradlew.bat이 내장 gradle을 실행시키기 위한 쉘스크립트이며 gradlew는 유닉스 기반, gradlew.bat은 윈도우 용 스크립트이다.
# gradlew를 사용해서 빌드하는 명령어
./gradlew build
Layered JAR(계층형 JAR)는 JAR 파일을 논리적으로 분리하여 런타임에 모듈화 및 커스터마이징할 수 있도록 하는 기술이다.
[공식문서]Spring Boot Gradle Plugin Reference Guide - Packaging Layered Jar or War
전통적인 JAR 파일(Fat Jar)은 모든 클래스 및 리소스가 묶여 있으며, 클래스 패스에 추가될 때 한 번에 로드된다. 반면, Layered JAR는 JAR 파일을 여러 레이어로 나누어 각 레이어에는 모듈화된 클래스, 리소스 및 구성 파일이 포함된다. 이를 통해, 런타임에 필요한 모듈만 로드하고, 필요한 경우 레이어를 추가하거나 제거하여 동적으로 애플리케이션을 구성할 수 있다.
Fat Jar란?
Fat Jar는 애플리케이션의 모든 의존성을 단일 JAR 파일에 패키징하는 방법이다. 이 방법은 애플리케이션을 배포할 때 의존성 파일을 별도로 다운로드하거나 설치하지 않고도 애플리케이션을 실행할 수 있게 해준다. 이를 위해, 빌드 도구(예: Maven, Gradle)가 애플리케이션의 코드와 모든 의존성을 함께 패키징하여 하나의 JAR 파일로 만든다.
하지만 이 방법은 애플리케이션의 크기가 매우 크기 때문에 다운로드와 배포에 많은 시간이 걸릴 수 있다. 또한, 의존성이 충돌하는 경우에는 해결하기가 어려울 수 있다. 그래서 최근에는 더 작은 크기의 애플리케이션과 더 높은 호환성을 위해 Layered JAR와 같은 다른 패키징 방법이 등장하고 있다.
Layered JAR를 사용하면 애플리케이션을 더 빠르게 시작할 수 있다. 또한, 필요한 모듈만 로드하므로 메모리 사용량이 줄어들고, 배포 및 업데이트가 더 쉬워진다. 이러한 이점으로 인해, 최근에는 Java 애플리케이션을 개발할 때 Layered JAR를 사용하는 것이 권장되고 있다.
Layered JAR는 Java 9부터 도입된 모듈 시스템(Java Module System)의 일부 기능이다. 따라서 Layered JAR를 사용하려면 Java 9 이상의 버전이 필요하다.
그리고 spring boot 2.3.0 이상부터 layer 기능을 지원한다.
layered jar로 jar 파일을 만들게 되면 4가지 폴더로 구성되어 있다.
여기서 자주 변경되는 layer가 있고, 자주 변경되지 않는 layer도 있다.
뒤에 언급된 layer일 수록 변경이 적다.
기존 프로젝트가 Maven으로 Layered Jar 방식을 사용해 빌드하고 있었는데 이 사용법이 꽤 귀찮다.
pom.xml에 메이븐 플러그인을 설정해주고
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
여기에 설정되어 있는 layers.xml 파일을 작성해야 한다.
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-2.6.xsd">
<application>
<into layer="spring-boot-loader">
<include>org/springframework/boot/loader/**</include>
</into>
<into layer="application" />
</application>
<dependencies>
<into layer="application">
<includeModuleDependencies />
</into>
<into layer="snapshot-dependencies">
<include>*:*:*SNAPSHOT</include>
</into>
<into layer="dependencies" />
</dependencies>
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>snapshot-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>
인터넷을 뒤지다보니 gradle에서 layered jar로 빌드하기 위해선 build.gradle에 다음 스크립트를 추가해줘야 한다고 되어 있었다.
# 결론부터 말하자면 Spring Boot 2.3 이상이면 이 설정도 해줄 필요 없다.
bootJar {
enabled = true
layered()
}
하지만 이것은 Spring Boot 2.3 버전 이하에서 사용하기 위한 것이고, Spring Boot 2.3 버전부터는 bootJar 테스크에서 layered() 설정이 기본적으로 활성화되어 있다. 따라서 별도의 설정 없이 bootJar를 실행하면 자동으로 Layered JAR가 생성된다.
이전 버전과 달리 enabled 속성을 따로 설정할 필요도 없다. 다만, 레이어의 구성 방식이나 설정 내용을 변경하려면 bootJar { layered() } 설정을 사용하여 필요한 설정을 추가하거나 수정해 주어야 한다.
편리해졌어!👍
덕분에 gradle로 변경하면서 필요없는 xml을 싹 날릴 수 있게 되었다.
이 jar의 layer를 이용해서 docker image build를 최적화 할 수 있다.
도커는 layer 별로 캐시를 관리한다. 즉, 변경이 없다면 캐시를 사용해서 빌드에 사용하기 때문에 리소스도 적게 들고 빌드 속도 빨라지게 되는 것이다.
그렇다면 jar를 layer로 나눠서 Docker build 하려면 어떻게 해야하나?
이렇게 Dockerfile을 만들어주면 된다.
FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR application
COPY ./my-back/build/libs/app-0.0.1-SNAPSHOT.jar application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM adoptopenjdk:11-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-Duser.timezone=Asia/Seoul", "org.springframework.boot.loader.JarLauncher"]
중요한 부분을 한 줄씩 해석해보자.
4번 라인에 있는
java -Djarmode=layertools -jar application.jar extract
이 명령어는 빌드된 jar를 layertools로 추출한다는 뜻이다. 이렇게 하면 위에서 언급한 4개의 layer로 jar가 분리되게 된다.
그리고 두 번째 FROM 지시어 부분에서는 이전 단계에서 추출한 layer들을 모두 COPY 해온다. 여기서 중요한 것은 copy 순서를 자주 바뀌지 않는 것부터 자주 바뀌는 순으로 해주는 것인데, 이렇게 하는 이유는 자주 바뀌는 부분을 나중에 가져와야 캐시가 깨질 가능성이 적어지므로 캐시 효율을 높일 수 있기 때문이다.
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
가져온 layer들로 docker build를 수행하고 dev profile을 주입해 jar를 실행시킨다.
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-Duser.timezone=Asia/Seoul", "org.springframework.boot.loader.JarLauncher"]
이 layer 방식을 통해서 어떤 부분이 수정되더라도 최소한의 범위에서 캐시가 깨지기 때문에 효율적으로 docker image를 build 할 수 있는 것이다.