목표

간단한 Spring Boot 앱을 만들겁니다.
Fat Jar 가 아니라 내 코드만 들어간 작은 Jar 를 만들 겁니다.
디펜던시 jar 파일들은 deps 디렉토리에 다운받을 겁니다.
내 jar 파일과 deps 를 classpath 에 걸고 앱을 실행시킬 겁니다.
이 과정을 돕는 Gradle 스크립트를 Kotlin DSL 로 만들겁니다.

배경

거의 10 년만에 Java + Spring 을 구경하고 있습니다.

2010 년. Spring 3 시절에는 Servlet Container, Tomcat, war 파일 같은 정책을 따라 웹 어플리케이션을 만들었습니다. Java 코드를 컴파일해서 jar 파일을 만듭니다. jar 파일과 HTML등 리소스들을 묶어서 war 파일을 만듭니다. war 파일을 Tomcat같은 서블릿 컨테이너에 복사합니다. 톰켓이 방금 받은 war 파일을 다시 풉니다. 톰켓이 내 코드를 실행합니다. 이것이 정석적인 루트였습니다.

개발과정에서 불필요한 반복 작업이 많았습니다. 불필요한 반복을 피할 방법을 궁리했습니다. 당시 Jetty 라는 Embeded Web Server 가 나오기 시작했습니다. 평범한 jar 파일로 존재합니다. 서블릿 컨테이너를 거치지 않고 내 코드에서 Jetty 를 실행시킬 수 있었습니다. war 파일을 만들고 디플로이하는 과정이 사라졌습니다. 우여곡절이 꽤 있었지만 나름 순조로운 개발이 가능했습니다.

2011 년. Node.js + Express 를 봤습니다. 제가 원하던 개발 프로세스를 기본으로 하고 있었습니다. 코드 만들고, 실행하고, Ctrl-C 로 중지. Node 로 이주해서 Express 를 꽤 오래 썼습니다.

2019 년의 Java 세계에선 뭘하고 있을까 궁금해졌습니다. Spring 5 가 있군요. 얼핏보니 Servlet 컨테이너를 치우고 Express 식 방식을 쓰는 것 같습니다. Spring Boot 라는 도우미셋도 있는 것 같습니다. 잠시 구경을 하기로 합니다.

어쩌다 보니 Gradle 로 Spring 데모 어플리케이션을 빌드하고 있습니다. Gradle 도 처음 봅니다. Gradle 빌드 과정에서 Spring Boot Gradle Plugin 을 쓰라는 것 같습니다. 순수 Gradle 기능만으로 jar 를 떨구고 실행하면 되지 않나? 얘는 뭐하는 놈인가? 내 코드와 디펜던시 jar 들을 묶어서 빌드 때마다 거대한 jar 를 만든답니다.

자바는 변하지 않는 것 같습니다. 묶는 것을 너무 좋아합니다. 해서 이 과정을 또 없애 보기로 합니다.

Java, Spring 공부를 다시 시작한지 며칠 밖에 되지 않았습니다. 이렇게 하는 것이 좋은지, 맞는지, 아니면 해서는 안 되는 것인지 잘 모르겠습니다. 그냥 기억이 사라지기 전에 간단히 삽질 결과를 적어둡니다.

리포지터리

https://github.com/drypot/spring-webflux-study

Spring + Gradle 학습을 위해 만든 리포지터리입니다.

버전

오늘 날짜는 2019년 9월 20일 입니다.
아래 버전들을 쓰고 있습니다.
Spring Boot 2.1.8.
Gradle 5.6.2.

Spring Boot

스프링 어플리케이션을 만들려면 많은 jar 들을 끌고 와야 합니다.
버전 충돌이 생기고 초기화도 복잡합니다.
이 라이브러리들의 관리를 돕기 위해 나온 것이 Spring Boot,
라고 현재 이해하고 있습니다.
틀린 것일지도 모르겠습니다.

어쨌든,
제 눈에 Spring Boot 는 라이브러리 디펜던시 정의 pom.xml,
빌드 플러그인, 런타임 라이브러리로 구성되어 있습니다.

문제는 빌드 플러그인입니다.
내 코드와 라이브러리 코드를 모아서 빌드 때마다 Fat Jar 를 만듭니다.
다른 옵션은 없는 것 같습니다.

Gradle Kotlin DSL

Gradle 은 빌드 도구입니다.
원래 Groovy 를 스크립팅 언어로 사용했습니다.
얼마전부터 Kotlin 을 지원하는 것 같습니다.
저는 Kotlin DSL 을 써보고 있습니다.

Plugins

plugins {
  id("org.springframework.boot") version "2.1.8.RELEASE" apply false
  id("io.spring.dependency-management") version "1.0.8.RELEASE"
  java
  application
}

스프링 부트 어플리케이션을 빌드하려면 최소 이정도 빌드 플러그인이 필요합니다.

org.springframework.boot 에 Gradle 빌드 플러그인이 들어있습니다. 연결은 하되 쓰지 않을 것이므로 apply false 했습니다. 혹시 나중에 쓸 일이 생길 것 같아 그대로 두었습니다. 아래 디펜던시 플러그인에 스프링 부트 버전을 상수로 던질 수 있다는 장점도 있습니다.

io.spring.dependency-management 는 디펜던시 플러그인입니다. 빌드 플러그인과는 떨어져있는 것 같습니다.

Dependency Management

dependencyManagement {
  imports {
    mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
  }
}

빌드 플러그인을 동작시키면 위에 적은 dependencyManagement 항목을 자동으로 처리합니다.
하지만 쓰지 않을 계획이니 명시적으로 적었습니다.

dependencyManagement {
  imports {
    mavenBom("org.springframework.boot:spring-boot-dependencies:2.1.8.RELEASE")
  }
}

mavenBom 을 콜할 때 상수를 넘겼는데 스트링을 직접 써줘도 됩니다.

참고: https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/#managing-dependencies-customizing

Gradle 'jar' Task

gradle jar 하면 내 코드만 들어있는 jar 가 떨어집니다.

스프링 부트 플러그인을 동작시켰으면 bootJar 태스크가 생성됩니다.
Gradle 기본 jar 태스크는 disable 됩니다.

여기선 스프링 부트 플러그인을 쓰지 않으므로
jar 관련 설정은 Gradle 기본 방식을 따르면 됩니다.
현재로서는 해줄 것이 없습니다.

Gradle 'run' Task

application {
  mainClassName = "com.drypot.App"
}

gradle run 하면 어플리케이션이 실행됩니다.

스프링 부트 플러그인을 동작시켰으면 bootRun 태스크가 생성됩니다.
여기선 없으니 대신 Gradle 기본 run 태스크를 씁니다.
run 태스크는 application 플러그인이 만듭니다.
설정은 Gradle 기본 방식을 따르면 됩니다.

Gradle 'copyDeps' Task

tasks.register<Delete>("deleteDeps") {
  delete("deps")
}

tasks.register<Copy>("copyDeps") {
  dependsOn("deleteDeps")
  from(configurations.default)
  into("deps")
}

Gradle 없이 앱을 실행시키려면 디펜던시 목록을 jvm 에 넘겨줘야 합니다.
gradle run 외에 이 과정을 편하게 하는 방법은 아직 모르겠습니다.
java -cp "deps/*" 식으로 하기 위해서 copyDeps 태스크를 만들어 봤습니다.

jar 들을 다운 받아서 deps 폴더에 쫙 넣어줍니다.
가끔 한번만 실행하면 됩니다.

Run from Command-line

java -cp "deps/*:build/libs/spring-webflux-study-0.0.1-SNAPSHOT.jar" com.drypot.App

내 jar 파일도 떨궜고 deps 도 채웠습니다.
쉘에서 어플리케이션을 실행해 봅니다.