본문 바로가기
  • 개발하는 곰돌이
Development/etc.

[Gradle] 순수 자바/코틀린 Gradle 프로젝트를 실행 가능한 jar 파일로 빌드하기

by 개발하는 곰돌이 2023. 11. 28.

목차

    들어가기 전에

    종종 스프링부트를 사용하지 않은 간단한 순수 자바/코틀린 프로젝트를 빌드해서 실행할 일이 있다. 스프링부트를 사용하지 않는 경우에는 bootJar 명령이 없어서 jar 명령으로 빌드할 수밖에 없는데 이렇게 생성된 jar 파일은 java -jar 명령어로 실행해도 제대로 실행되지 않는다.

    main manifest 속성이 없다면서 실행되지 않는다.

    이러한 현상이 발생하는 이유와 해결법을 정리해두고자 한다.

    gradle jar로 생성한 jar 파일은 실행되지 않을까?

    기본적으로 Gradle의 jar 명령은 프로젝트의 소스 파일들을 컴파일한 후 jar 파일의 형태로 압축한다. 그리고 이 과정에서 MANIFEST.MF 파일이 생성되어 jar 파일에 추가된다.

     

    터미널에 jar tf {jar 파일명}을 입력하면 jar 파일의 내부 구조를 확인해볼 수 있는데 여기서 META-INF라는 디렉토리 안에 MANIFEST.MF가 생성되어 있는 것을 볼 수 있다.

    그리고 터미널에 unzip -p {jar 파일명} META-INF/MANIFEST.MF를 입력하면 META-INF 디렉토리에 있는 MANIFEST.MF의 내용을 볼 수 있다.

    만약 bash: unzip: command not found가 출력된다면 centOS 기준으로 sudo yum install unzip을 입력하여 unzip을 설치한 후 시도하면 된다.

    MANIFEST.MF의 내용을 보면 뭔가 이상하다. 매니페스트 버전만 나와있고 아무런 내용이 없다. bootJar로 빌드하여 정상적으로 실행되는 스프링부트 jar 파일의 내용과 비교해 보자.

    메인 클래스가 무엇인지를 비롯해 여러가지 정보가 담겨있는 것을 확인할 수 있다.

     

    여기까지 내용을 정리해보면 jar 파일을 실행하기 위해선 반드시 매니페스트에 메인 클래스 정보가 있어야 하는데, 그래들의 기본 jar 명령은 단순히 프로젝트의 소스만 자바 바이트 코드로 컴파일하여 처리하기 때문에 실행이 되지 않는다는 사실을 유추할 수 있다.

    외부 라이브러리도 추가되지 않는다!

    사실 매니페스트만 문제가 되는게 아니다. build.gradle을 통해 Unirest를 의존성에 추가한 후 jar 명령을 사용하여 jar 파일을 생성한 다음 자바 디컴파일러 툴을 사용하여 내부를 확인해보자.

    인텔리제이에서는 프로젝트의 외부 라이브러리로 unirest와 gson이 추가되어 있다.
    디컴파일러로 확인해보면 라이브러리가 없다.

    그런데 디컴파일러로 확인한 jar 파일 내부에는 외부 라이브러리가 전혀 추가되어있지 않다. 이러면 매니페스트에 메인 클래스의 정보가 추가되더라도 외부 라이브러리의 클래스를 사용하는 기능은 동작하지 않는다.

    jar 파일에 메인 클래스 정보를 추가해주자

    우선 jar 파일이 메인 클래스를 찾지 못하는 문제는 build.gradle 또는 build.gradle.kts에 아래 내용을 추가하여 해결할 수 있다.

     

    build.gradle

    jar {
        manifest { attributes 'Main-Class': '{메인 메소드가 포함된 클래스 명}' }
    }

     

    build.gradle.kts

    tasks.jar {
        manifest { attributes["Main-Class"] = "{메인 메소드가 포함된 클래스 명}" }
    }

     

    클래스 없이 최상위레벨에 메인 메소드를 작성한 코틀린 프로젝트의 경우에는 메인 메소드가 작성된 파일명 + Kt가 메인클래스 명이 된다. (ex. Main.kt 파일에 메인 메소드를 작성했다면 메인 클래스 명은 MainKt가 된다.)

     

    해당 내용을 적용한 후 jar 파일을 생성하여 MANIFEST.MF의 내용을 확인해보면 메인 클래스의 경로가 추가되어 있고, java -jar 명령어를 사용하여 정상적으로 파일을 실행할 수도 있다.

    메인 클래스 정보가 정상적으로 등록되어 있고, 실행도 잘 된다.

    외부 라이브러리도 추가해주자

    아래와 같이 단순히 Unirest 라이브러리에 존재하는 JSONObject 객체를 생성하는 코드를 추가하고 jar 파일을 생성하여 실행해보자.

    import kong.unirest.json.JSONObject
    
    fun main() {
        val json = JSONObject()
        println("Hello, World!")
    }

    JSONObject 클래스를 찾을 수 없다고 한다. 앞서 언급한 것처럼 jar 파일 내부에 라이브러리가 없기 때문이다.

     

    jar 파일에 라이브러리를 추가하기 위해 build.gradle 또는 build.gradle.kts에 아래 내용을 추가한다.

     

    build.gradle

    jar {
        ...
        from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    }

     

    build.gradle.kts

    tasks.jar {
        ...
        from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    }

     

    duplicatesStrategy 속성은 jar 파일 생성 과정에서 중복되는 파일을 처리하는 정책을 결정하는 것인데 위 코드에서는 중복된 파일을 무시하는 방향으로 진행했다.

     

    DuplicatesStrategy (Gradle API 8.4)

    Do not attempt to prevent duplicates. If the destination of the operation supports duplicates (e.g. zip files) then a duplicate entry will be created. If the destination does not support duplicates, the existing destination entry will be overridden with th

    docs.gradle.org

     

    여기까지 진행하고 jar 파일을 생성하여 자바 디컴파일러로 확인해보면 라이브러리가 추가된 것을 확인할 수 있다.

    라이브러리가 정상적으로 추가되어 있다.

    마치며

    사실 요즘 대부분의 자바 애플리케이션은 스프링부트를 사용하다보니 실제로 이 글의 내용을 사용할 일은 많지 않다고 생각한다.

     

    하지만 굳이 스프링부트와 같은 프레임워크를 사용할 필요가 없는 가벼운 자바 애플리케이션을 장난삼아(?) 개발해서 빌드할 일이 있다면 유용하지 않을까 싶다.

    댓글