Spring Fat-jar for Spark

Spark에서 Spring을 실행할 수 있게 Fat Jar빌드하는 방법에 대해 기술합니다.

How to Build Fat Jar

Build Tool을 어떤 종류를 사용하는지에 따라 방법이 달라집니다.
Maven은 maven-assembly-plugin
Gradle Shadow Plugin 을 사용해야합니다.
대부분은 위의 2개 Plugin중 하나만 사용하면 해결되지만, Spring Boot는 구동방식이 달라서, 별도의 처리가 필요합니다.

Boot-Jar

Spring에서 공식적으로 권장하는 방법입니다.

설명에 따르면 Java는 nested-jar파일을 load하는 방법을 제공하지않습니다. 이때문에 압축 풀지않고, 실행해야하는 환경이라면 문제가 될 수 있습니다.
이 때문에 Shaded Jar(Fat Jar)를 사용해야합니다. 모든 Jars파일로부터 모든 Class를 모아서 Pacakage하여 하나의 Jar파일로 만들어 줍니다.
실제로 Application에 존재하는 library를 보기에 힘들어지기때문에, Spring은 다른 방법을 제공합니다.

어떤방식으로 구현되었는지가 궁금하다면 상세한 내용은 공식문서를 참고해주세요.

Configuration

1. Module 추가

implementation 'org.springframework.boot:spring-boot-loader'
위의 모듈은 Spring boot 를 Excutable Jar 또는 War로 만들수 있게 도와줍니다.

2. Manifest 변경

JarLauncher를 통해 실행되어야하기 때문에, Manifest 변경이 필요합니다.
META-INF/MANIFEST.MF 에 내용이 존재하는데. Build하며 기록되어야 합니다.

공식문서에는 아래와같이 가이드 하고 있습니다.

1
2
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.mycompany.project.MyApplication

저의 경우에는 아래와 같이 적용하였습니다.

1
2
3
4
5
6
jar {
    manifest {
        attributes "Main-Class": "org.springframework.boot.loader.JarLauncher"
        attributes "Start-Class": "com.mycompany.team.batch.SparkSpringBatchClient"
    }
}

문제점

java -jar xxx.jar 명령어로 실행할 경우, 정상동작합니다.
하지만 Spark에서 실행할 경우, 문제가 발생합니다.

이유는 Dependency version 때문이었습니다.
재직중인 회사에서 운영중인 Spark는 2.4버전을 사용 중인데, 여기에 포함되어있는 gson 버전이 Application에서 사용해야할 버전보다 낮았습니다. 이때문에 구버전의 Gson으로 사용되면서 Application구동 실패 하였습니다.

강제로 최신버전의 Gson을 사용할 수 있는 방안을 찾아보았습니다.
첫번째로는 사용할 Dependency버전을 명시하는 방법이였습니다.
(참고로 이 방법을 사용하지않아도 비교적 최신 gson을 사용합니다.) 하지만 spark에 내장되어있는 library를 우선으로 사용하기때문에 실패하였습니다.

두번째 방법은 Spark공식문서에서 제공하는 방법입니다. (참고)
spark.driver.userClassPathFirst
spark.executor.userClassPathFirst
위의 2개 변수는 기본값이 False로 되어있습니다.

문서에 따르면, 사용자가 옵션으로 추가한 --jars 를 우선시 사용한다고 되어있습니다.
시험 기능인 문제도 있고, 추가해야할 jar이 증가하면 관리할 포인트가 증가할 수 있습니다. 의도한대로 동작하지도 않았고, 의도했던 바가 아니라 더이상 사용하지 않았습니다.

이를 해결하려면 relocate(임의로 이름변경?)을 사용해야하는데, Shadow Plugin에서는 지원합니다.

Shadow Plugin

프로젝트 내에 있는 Dependency Classs와 Resource를 하나의 Jar로 만들어 줍니다.
이로 인해 얻을수 있는 장점은 2가지입니다.

  1. 실행가능한 Jar배포를 만든다.
  2. Library에 있는 공통 Dependency를 bundling, relocating하여 classpath conflict를 회피한다.

장점을 좀더 잘 풀어쓴글을 참고하면 좋겠습니다.

Add Option For Spring

Shadow Plugin으로 Spring Application을 Build하려면 추가 옵션이 필요합니다.

아래의 내용을 추가합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import com.github.jengelman.gradle.plugins.shadow.transformers.*
shadowJar {

    // Required for Spring
    mergeServiceFiles()
    append 'META-INF/spring.handlers'
    append 'META-INF/spring.schemas'
    append 'META-INF/spring.tooling'
    transform(PropertiesFileTransformer) {
        paths = ['META-INF/spring.factories' ]
        mergeStrategy = "append"
    }

}

상세한 내용은 Git Issue를 읽어보면 알 수 있습니다.

Relocating Packages

Classpath에 동일한 Dependency가 존재하여, 의도하고자한 버전을 사용하지 못하는 상황일때 사용하는 기능입니다. Hadoop, Spark에서 빈번히 발생합니다.

이 아이디어는 간단합니다. 충돌나는 Dependency를 Project에서 찾아낸뒤, 이름(경로)을 바꿔줍니다. 다른 이름의 Dependency를 사용하기때문에 문제가 생기지 않습니다.

구현방법은 간단합니다.

1
2
3
4
// Relocating a Package
shadowJar {
   relocate 'junit.framework', 'shadow.junit'
}

juni.frameworkshadow.junit 으로 변경해 줍니다.
필요에 맞게 아래처럼 적용합니다.

1
2
relocate 'com.google.gson', 'shadow.google.gson'
relocate 'com.fasterxml.jackson', 'shadow.fasterxml.jackson'

Shadow Plugin에서 relocation을 살펴보고싶다면 링크를 참고해주세요.

Classpath 변경

사실 여기까지 진행하고 나서, 잘 실행하는것을 기대했습니다.
하지만 java -jar project-all.jar 로 실행해보면, 이상함을 느낄 수 있습니다.
캡처

Spring자체는 실행되었지만, Main Application이 실행되지 않은 것을 확인 할 수 있습니다.
찾아보니, 원인을 알기 쉽지 않았습니다.

다행히 try and error 거치며 알아냈는데, 아래의 링크와 관련있었습니다. https://imperceptiblethoughts.com/shadow/configuration/dependencies/

기본으로 runtimeClassPath만 포함되서 Build되었고, 아래처럼 compileClassPath를 포함하니 해결되었습니다.

1
configurations = [project.configurations.compileClasspath, project.configurations.productionRuntimeClasspath]

문제해결이 우선이라, 왜 compileClassPath도 포함해야만 정상동작하는지는 확인해보지 않았습니다.

Conclusion

Spring을 사용할때, 어떻게 Fat Jar(Shaded Jar)을 빌드할 수 있는지 살펴보았습니다.
Boot-Jar은 스프링에서 권장하는 방법이고 간단하다는 장점이 있지만, Dependency 충돌하는 상황이라면 해결할기 어렵다는 문제가 있습니다.
이런경우는 하둡 클러스터를 사용하는경우에 해당됩니다.

Shadow Plugin을 사용하는 방법은 일반적으로 많이 사용하는 방법입니다.
하지만 스프링에 적용하려면 스크립트를 추가로 작성해주어야 하지만, Boot-Jar에서 겪었던 문제를 회피할 수 있습니다.

Spark나 하둡 클러스터를 사용해야하는 경우라면 Shadow Plugin을 사용하는 것을 권장합니다.
하지만 이와 같은 문제를 부딪힌 상황이 아니라면, 본인의 상황에 맞게 사용하는 것을 권장합니다.

Reference

Hugo로 만듦
JimmyStack 테마 사용 중