Jar 파일의 빌드와 배포
이번 포스팅에는 Spring Boot 없이 Jar 파일을 빌드 및 배포해 본 뒤, Spring Boot를 이용하여 편리하게 Jar 파일을 빌드 및 배포해 보면서 Spring Boot가 제공하는 편리성을 알아보도록 하겠습니다.
본 포스팅에서 사용한 코드의 패키지 구조는 아래와 같습니다. 각 클래스의 코드는 이 포스팅을 참고하시거나, 전체 소스 코드를 참고하시면 됩니다.
자바 Jar 파일 배포하기
자바의 main() 메서드를 실행하기 위해서는 jar 형식으로 빌드해야 합니다. 그리고 jar 안에는 META-INF/MANIFEST.MF 파일에 실행할 main() 메서드의 클래스를 아래와 같이 지정해야 합니다.
Gradle의 도움을 받으면 이 과정을 쉽게 진행할 수 있습니다.
//일반 Jar 생성
task buildJar(type: Jar) {
manifest {
attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
}
with jar
}
위 프로젝트를 아래 명령어를 통해서 실행하면 jar가 빌드되면서 jar 파일(embed-0.0.1-SNAPSHOT-jar)이 만들어지는 것을 확인할 수 있습니다.
./gradlew clean buildJar
하지만 이 jar 파일을 실행하면 오류가 발생합니다.
해당 문제를 확인하기 위해 jar 파일의 압축을 풀어 ($ jar -xvf embed-0.0.1-SNAPSHOT.jar) 내용물을 확인해 보면 아래와 같은 구조를 가지는 것을 확인할 수 있습니다. (참고로, MySpringBootMain 클래스 파일은 이 프로젝트에는 사용되지 않습니다.)
- META-INF
- MANIFEST.MF
- hello
- servlet
- MyServlet.class
- embed
- EmbedTomcatSpringMain.class
- spring
- MyConfig.class
- MyController.class
- servlet
중요한 것은, JAR를 푼 결과에 스프링 라이브러리나 내장 톰캣 라이브러리가 전혀 포함되어 있지 않습니다. 그래서 오류가 발생했던 것입니다. 이는, Jar 파일이 Jar 파일을 포함할 수 없기 때문입니다.
WAR와 다르게 JAR 파일은 내부에 라이브러리 역할을 하는 JAR 파일을 포함할 수 없습니다. 이것이 JAR 파일 스펙의 한계인데, 그렇다고 해서 WAR를 사용할 수는 없습니다. WAR은 WAS 위에서만 실행할 수 있으니까요. 이를 해결하기 위한 대안으로 FatJar를 사용할 수 있습니다.
참고(WAR의 단점 및 해결)
- WAS (e.g. 톰캣)을 별도로 설치해야 합니다.
- 개발 환경 설정이 복잡합니다.
- WAS 연동하기 위한 복잡한 설정이 필요합니다.
- 배포 과정이 복잡합니다. WAR를 생성하고 이를 WAS에 전달해서 배포해야 합니다.
- 톰캣의 버전을 업데이트 하려면 톰캣을 다시 설치해야 합니다.
JAR는 이러한 단점을 해결해 줍니다.
- 톰캣 같은 WAS가 라이브러리로 jar 내부에 포함되어 잇어 WAS를 별도로 설치하지 않아도 됩니다.
- 복잡한 WAS 설정 없이 main() 메서드만 실행하면 됩니다.
- 배포 과정이 단순합니다. JAR을 생성하고 원하는 위치에서 실행만 하면 됩니다.
- 톰캣의 버전이 업데이트 되면, gradle에서 내장 톰캣 라이브러리 버전만 변경하고 빌드 후 실행하면 됩니다.
FatJar (uber jar)
Jar안에는 Jar를 포함할 수는 없지만, 클래스는 포함할 수 있습니다. 라이브러리를 포함하는 Jar를 풀면 그 라이브러리들을 구성하는 class들이 나오는 것을 확인할 수 있는데, 이 class들을 뽑아서 새로 만드는 jar에 포함하면 됩니다. 이렇게 수많은 라이브러리에서 나오는 class 때문에 fat(뚱뚱한) jar가 생성되는 것입니다.
Fat Jar를 생성하기 위해서는 build.gradle에 아래 코드가 추가되어야 합니다.
//Fat Jar 생성
task buildFatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
}
duplicatesStrategy = DuplicatesStrategy.WARN
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
그 후, 다시 jar를 빌드하여 실행해 보겠습니다.
jar 빌드
jar 실행
[실행 결과] - 정상 동작
FatJar를 명령어 ($ jar -xvf embed-0.0.1-SNAPSHOT.jar) 로 풀어보면 엄청나게 많은 클래스들이 포함되어 있는 것을 확인할 수 있습니다.
Fat Jar의 장점
- 하나의 jar 파일에 필요한 라이브러리들을 내장할 수 있습니다.
- 내장 톰캣 라이브러리를 jar 내부에 내장할 수 있어, 배포부터 웹 서버 설치 및 실행까지 모든 것을 단순화할 수 있습니다.
Fat Jar의 단점
- 어떤 라이브러리가 포함되어 있는지 확인하기 어렵습니다.
- 모두 class로 풀려 있어, 어떤 라이브러리가 사용되고 있는지 파악하기 어렵습니다.
- 파일명 중복이 발생하여 문제가 발생할 수 있습니다.
- 클래스나 리소스 명이 같은 경우 하나의 파일을 포기해야 합니다. 만약 A 라이브러리와 B 라이브러리가 특정 파일을 사용한다고 할 때, 두 라이브러리 모두 해당 파일을 jar 안에 포함하고 있습니다. Fat Jar를 만들면 파일명이 같기 때문에 A, B 라이브러리 둘 다 가지고 있는 파일 중 하나의 파일만 선택되어 나머지 하나는 포함되지 않게 됩니다. 결과적으로 정상 동작하지 않습니다.
이제 Fat Jar가 아닌 내장 톰캣이 포함된 Spring Boot를 직접 빌드함으로써 위에서 발견한 여러 가지 문제점들을 해결할 수 있습니다.
이번에는 아래 패키지 구조를 가지는 클래스들을 사용할 것이고, embed 폴더 내EmbedTomcatSpringMain.class 는 모두 주석처리 해야 합니다. (해당 포스팅 혹은 전체 소스 코드 통해 아래 클래스들 코드를 참고할 수 있습니다.)
또한 build.gradle을 아래와 같이 수정합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
//스프링 MVC 추가
// implementation 'org.springframework:spring-webmvc:6.0.4'
//내장 톰켓 추가
// implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
jar 빌드
jar 실행
[실행 결과] - 정상 동작
FatJar로 빌드하지 않았음에도 불구하고 정상적으로 동작하는 것을 볼 수 있습니다.
Spring Boot가 만드는 Jar 파일에는 어떤 비밀이 숨겨져 있는지 아래에서 살펴보겠습니다.
스프링 부트 jar 분석
해당 jar 파일의 압축을 풀어 내용물을 살펴보면 아래와 같은 구조를 가집니다.
각 폴더 내에는 아래와 같은 파일들이 담겨져 있습니다.
- BOOT-INF
- classes: 우리가 개발한 class 파일과 리소스 파일
- lib: 외부 라이브러리
- classpath.idx: 외부 라이브러리 경로
- layers.idx: 스프링 부트 구조 경로
- META-INF
- MANIFEST.MF
- org/springframwork/boot/loader
- JarLauncher.class: 스프링 부트 main() 실행 클래스
확인해보면, jar 내부에 jar를 담아서 인식하는 것이 불가능한데 jar가 포함되어 있습니다. 이게 가능한 이유는 Spring Boot가 jar 내부에 jar를 포함할 수 있는 특별한 구조의 jar를 만들고 이를 실행할 수 있도록 하는 실행 가능 jar(Executable Jar)를 만들기 때문입니다.
참고로, 실행 가능 Jar는 자바 표준은 아니고, Spring Boot에서 새롭게 정의한 것입니다.
Jar 실행 정보 (MANIFEST.MF)
java -jar xxx.jar를 실행하면서 META-INF/MANIFEST.MF 파일을 찾고, 여기에 명시된 Main-Class를 읽어서 main()를 실행하게 됩니다. MANIFEST.MF 내부를 보면 아래와 같습니다.
- Main-Class: 우리가 작성한 main()을 포함한 클래스가 아니라, JarLauncher라는 클래스 입니다.
- JarLauncher는 Spring Boot가 빌드 시에 자동으로 넣어줍니다.
- 이를 포함시키는 이유는, Spring Boot가 Jar 내부에 Jar를 포함하는 특별한 구조로 Jar를 만들었기 때문에 이를 읽어들이는 기능이 필요하기 때문입니다. JarLauncher가 이런 일을 처리한 다음 Start-Class에 지정된 main()을 호출하게 됩니다.
- JarLauncher 클래스는 org/springframework/boot/loader/ 에 포함되어 있습니다.
- Start-Class: 우리가 작성한 main()이 포함된 클래스 입니다.
- 기타 정보들: Spring Boot가 내부에서 사용하는 정보들 입니다.
- Spring-Boot-Classes: 개발한 클래스 경로
- Spring-Boot-Lib: 라이브러리 경로
- Spring-Boot-Classpath-Index: 외부 라이브러리 모음이 포함된 경로
- Spring-Boot-Layers-Index: 스프링 부트 구조 정보가 포함된 경로
org/springframework/boot/loader
스프링 부트 로더라고 합니다. JarLauncher를 포함한 Spring Boot가 제공하는 실행 가능 Jar를 실제로 구동시키는 클래스들이 포함되어 있습니다. Spring Boot는 빌드시에 이 클래스들을 포함해서 실행 가능 Jar를 만듭니다.
BOOT-INF
WAR는 WEB-INF라는 내부 폴더에 사용자 클래스와 라이브러리를 포함하고 있는데, 실행 가능 Jar도 그 구조를 본떠 BOOT-INF를 만들었습니다. JarLauncher를 통해 여기에 있는 classes와 lib에 있는 jar 파일들을 읽어 들입니다.
실행 과정 정리
- java -jar xxx.jar
- MANIFEST.MF 인식
- JarLauncher.main() 실행
- BOOT-INF/classes/ 인식
- BOOT-INF/lib/ 인식
- MySpringBootMain.main() 실행
참고로, IDE에서는 필요한 라이브러리를 모두 인식하여 MySpringBootMain.main()을 바로 실행하기 때문에 JarLauncher가 별도 필요하지 않습니다.
이렇게 Spring Boot가 Jar 파일을 생성하여 어떻게 웹 애플리케이션을 실행하는지 확인하였습니다.
[참고자료]
김영한, " 스프링 부트 - 핵심 원리와 활용", 인프런