[Gradle] Ant / Maven To Gradle

HeeJun·2024년 4월 7일

오늘 이야기 할 내용은 신입 개발자로서 한 첫 번째 일에 대한 내용이다.

현재 내가 담당하고 있는 프로젝트는 데이터 수집 프로젝트로, 각 웹 페이지를 스크랩 하는 Main Class 를 Jar 로 빌드해 Admin 페이지에서 등록하고 스크랩 요청을 보내면, Job 을 등록하고 실행되는 방식이다.

처음 입사 후에 새롭게 스크랩이 필요한 페이지에 대해 스크랩 로직을 작성하고 매번 Jar 파일로 Build 하고 Admin 에 등록하고 하는 행위가 불합리하고, 번거로운 작업이라고 생각이 들어 RestAPI 를 설계해, Spring Service 객체를 Find 해 동작하는 방식으로의 변경을 제안했다.

현재의 레거시 프로젝트는 신규로 진행되는 프로젝트가 마무리 되면 사용을 중단할 예정이고, 현재 스크랩하는 웹 페이지가 1500개 정도 규모이고, 잦은 변동과 그에 따른 대응을 하기에는 각 스크랩 Class 를 Build 후 Admin 에 등록하는 환경을 유지해야 한다는 이유.
또한 하나의 프로젝트로 Build 되서 관리를 하려면, 기존 레거시 시스템에서 CI/CD 도입, 현재 Job 을 DB 에 등록해 주기적으로 Pooling 하는 방식에서 MQ 를 사용한 비동기 처리, 서버 스펙의 변동 등 대체될 프로젝트에 너무 많은 투자가 들어간다는 점을 근거로 제안은 거절되었다.

그렇다면 현재의 상황에서 잦은 Build 에 대한 개선할 부분을 생각해봤다.

현재 프로젝트는 jdk 1.8 + spring 4.x version 으로 진행된 프로젝트로, Build 는 Ant 로 진행하고, 의존성 관리는 Maven 으로 하고 있지만 사실상 Build 에 사용되는 라이브러리는 Jar File 형태로 따로 관리가 되고 있다.

각 스크립트는 Ant 를 이용한 Build 가 진행되고 이는 대략 스크립트 당 평균 40초 정도의 시간이 필요했다.

현재 데이터 수집을 담당하는 개발자는 7명이고, 한 사람이 하루에 진행하는 Build 수는 5건.
하루에 개발자 한 명이 소요하는 Build 시간이 3분이고, 팀 전체로는 21분, 5일 간 팀이 소요하는 Build 시간은 1시간 40분 정도.

그렇기에 Build 시간의 개선을 해보자는 목표를 세웠고, 최근 가장 많은 사용률과, 레퍼런스가 나오고 있는 Gradle 을 활용하기로 했다.

팀 내에서 Build 에 필요한 요구사항을 조사했다.

1. 각 스크립트의 Class 이름으로 된 Main Class 단위로 Build 가 되야한다.
2. 현재의 라이브러리 버전을 유지하고, 사용이 가능해야 한다.
3. Build 결과물은 Jar File 이고, 각각의 Jar File 은 20~30MB 의 용량을 유지해야 한다.

위 3 가지의 요구사항을 충족하면서 변경을 진행했다.

1. Gradle init

첫 시작은 프로젝트 내에서 gradle init command 를 실행하는 것이다.
현재 프로젝트는 pom.xml 이 작성되어 있고, Gradle 을 미리 설치 해놓았기에 gradle init command 를 실행하는 것으로 쉽게 Gradle 을 시작할 수 있다.

2. 라이브러리 Check

두 번째는 라이브러리를 확인했다.
기존에 사용하던 모든 라이브러리의 종류와 version 을 확인하고, gradle dependency 에 추가했다.

3. Build Script 작성

세 번째로는 Build Script 를 작성했다.

초기의 Build 는 shadow plugin 을 활용한 fatJar 형태로 Build 를 진행했다.
fatJar 는 Build 후 Jar File 의 용량이 120MB 였고, 이는 요구사항에 맞지 않아 다른 방법을 찾는 것이 필요했다.
해결한 방법을 설명하기 전에 Spring 환경에서 Build 된 Jar / War 의 실행 과정과, Spring Boot 와 내장 Tomcat 환경에서의 Jar 실행 과정, JVM 의 기본 ClassLoader 에 대한 간단한 설명이 필요하다.
0. JVM ClassLoader
Java 로 작성 된 Source Code 가 실행되는 과정은 아래와 같다

java source(.java) -> Java Compiler -> Java Byte Code(.class) -> JVM(ClassLoader) -> Java interpreter + JIT compiler -> Native Code

자세한 과정은 다루지 않고, JVM 위에서 실행되는 ClassLoader 에 맞춰보면 기본적으로 JVM 의 ClassLoader 는 Java Byte Code 즉, .class File Load 하기 위해 설계되었다.
기본적으로 Jar File 은 .class File 을 압축한 집합체이고, JVM ClassLoader 는 Jar File 을 실행 할 때 .class File 을 Load 한다.
1. Spring 환경
War Build & Excute
- War File 로 Build 하면 그 내부에 라이브러리 Jar File 을 포함하고 있다
- War File 은 기본적으로 WAS 위에서 동작한다. 서버의 JVM 위에서 Tomcat(WAS) 가 실행 (jvm system class loader 가 Tomcat 의 Main class 를 Load)
- Tomcat 내부의 ClassLoader (Jar File 을 해석하고 Load 가 가능) 가 War File 의 Main Class Load
- Main Class excute 이후 동적 ClassLoading 을 통해 라이브러리 Jar File 및 기타 class File Load
- War 내부의 Jar File 을 Load 할 수 있다
Jar Build & Excute
- 실행 가능한 fatJar 로 Build 시 모든 라이브러리를 class File 로 풀어낸 뒤 Jar 로 압축한다.
- JVM 의 classLoader 가 Jar File 내부의 Main Class Load
- Main Class excute 이후 동적 ClassLoading 을 통해 라이브러리 class File 및 기타 class File Load
- Spring 의 Jar File 은 JVM 의 기본 ClassLoader 를 사용하기 때문에 내부에 다른 Jar File 은 Load 할 수 없다
2. Spring Boot 환경
Jar Build & Excute
- Spring Boot 는 기본적으로 내장 Tomcat 을 가지고 있다
- Jar File 로 Build 하면 그 내부에 라이브러리 Jar File 을 포함하고 있다
- JVM 의 classLoader 가 Jar File 의 Main Class Load & Excute (JarLauncher)
- JarLauncher 가 실행되면서 LaunchedClassLoader 라는 새로운 ClassLoader 를 생성하고 등록
- LaunchedClassLoader extends JarUrlClassLoader extends URLClassLoader 의 관계
- 새로운 ClassLoader 가 등록되고 난 후 SpringBootApplication 실행
- 이후 동적 ClassLoading 을 통해 라이브러리 Jar File 및 기타 class File Load
- Jar File 내부에 다른 Jar File 을 Load 할 수 있다

3-1. Build Script 수정

용량 문제를 해결하기 위해 기존에 Build 된 Jar File 내부를 살펴봤다.
기존에 진행되던 Build Process 에서 용량 문제를 해결하기 위해 아래와 같이 Build 를 진행하고 있었다.
1. Build 되는 라이브러리는 각 version 에 맞는 Jar File 을 보관해 함께 Build
2. Jar 형태의 라이브러리를 Load 하고 사용 가능한 새로운 ClassLoader(Jar-in-Jar-Loader) 사용
새롭게 Build Script 를 작성하기 전에 위의 두 가지 사항에 대해 대응하기 위해 사전 작업을 진행했다.
1. 각 라이브러리 Jar File 을 project 의 src/main/resources/lib 경로에 저장
2. ClassLoader 가 라이브러리 Jar File 을 인식하고 그 내부의 .class 를 Load 할 수 있게 하는 작업
2번 작업을 진행하는 도중 새로운 사실을 알게 되었다. Jar File 을 인식하고 그 내부의 .class 를 Load 할 수 있는 URIClassLoader 가 이미 구현되어 있다는 것이였다.
기존에 Build Script 를 정의했던 사람의 경우에는 이 사실을 인지하지 못하고, Jar-in-Jar-Loader 라는 라이브러리를 함께 빌드하고 Jar File 실행 시 이 라이브러리의 main class 를 실행하도록 정의해두었다.
URIClassLoader 가 이미 정의 되어 있고 이는 Classpath 내에 존재하는 Jar file 을 인식할 수 있다는 점을 실제로 확인하기 위해 Build Script 를 새롭게 정의하고 실험을 진행했다.

build Script 를 새롭게 작성하고 진행한 build test 는 성공적이였다.
정상적으로 URIClassLoader 를 상속하는 ApplicationClassLoader 에서 jar 형태의 라이브러리를 로드하고 정상 실행이 가능했다.

결과적으로 Gradle 로 빌드를 진행했을 때 평균적으로 10초 정도의 Build Time 이 나왔고, 이는 기존 30~40초 에 비하면 시간적으로 많은 이득을 얻을 수 있었다.

build 된 Jar File 의 사이즈 또한 기존과 비슷한 25MB 의 용량을 유지할 수 있었다.

profile
내가 작성한 코드 한 줄로 누군가를 편하게

0개의 댓글