최근 Gradle에서는 코틀린 문법을 사용한 버전을 기본 언어로 채택하였다.
따라서 코틀린 + 스프링 프로젝트를 사용하면서, Gradle 설정도 Groovy를 사용하는 것이 아닌, 코틀린 문법으로 된 Gradle 설정을 사용하였다.
미래의 변화에 유연하게 대응하기 위함도 있고, 지금 진행 중인 스프링 프로젝트도 코틀린을 사용하므로 그대로 사용해 보기로 했다.
하지만 시작부터 난감하게 프로젝트를 빌드하니 에러를 마주했다. 😂
기존 groovy 문법을 사용했을 때 어떤 점에서 이득인지 간단하게 살펴보고, 기존 설정을 어떻게 바꿔야 하는지 알아보겠다.
기존 groovy 문법을 사용했을 때의 가장 큰 차이점은 컴파일 시점에 오류를 검사할 수 있다.
tasks.withType<Test> {
useJUnitPlatformz() // 오타를 컴파일 에러로 알려준다!
}
또한 코틀린 문법을 사용하기 때문에 IntelliJ를 사용한다면, 자동 완성 기능을 지원받을 수 있다!
그 외 간소화된 플러그인 구문 등이 있지만, 크게 체감은 되지 않았다.
Spring Initalizr를 사용하여 프로젝트를 세팅하면 기본 Gradle 설정이 다음과 같이 되어 있다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.2.0"
id("io.spring.dependency-management") version "1.1.4"
id("org.asciidoctor.jvm.convert") version "3.3.2"
kotlin("jvm") version "1.9.20"
kotlin("plugin.spring") version "1.9.20"
kotlin("plugin.jpa") version "1.9.20"
}
group = "kr.galaxyhub"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
extra["snippetsDir"] = file("build/generated-snippets")
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-mysql")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.test {
outputs.dir(snippetsDir) // 문법 오류
}
tasks.asciidoctor {
inputs.dir(snippetsDir) // 문법 오류
dependsOn(test) // 문법 오류
}
IntelliJ를 사용한다면 3개의 문법 오류가 있다고 친절하게 알려준다.
해당 컴파일 에러를 분석해 보면 snippetsDir
, test
변수가 정의되지 않았으므로 빌드가 되지 않는다.
따라서 다음과 같이 수정하면 된다.
...
// extra["snippetsDir"] = file("build/generated-snippets")
val snippetsDir = file("build/generated-snippets")
...
// 밑의 tasks.test와 같은 task이기 때문에 합쳐도 된다.
// tasks.withType<Test> {
// useJUnitPlatform()
// }
tasks.test {
outputs.dir(snippetsDir)
useJUnitPlatform()
}
tasks.asciidoctor {
inputs.dir(snippetsDir)
dependsOn(tasks.test)
}
스프링 공식 문서에는 다음과 같이 gradle 설정을 하라고 나와있다.
plugins { (1)
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
configurations {
asciidoctorExt (2)
}
dependencies {
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' (3)
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' (4)
}
ext { (5)
snippetsDir = file('build/generated-snippets')
}
test { (6)
outputs.dir snippetsDir
}
asciidoctor { (7)
inputs.dir snippetsDir (8)
configurations 'asciidoctorExt' (9)
dependsOn test (10)
}
여기서 1, 4, 5, 6, 7, 8, 10 설정은 되어 있기에 2, 3, 9 설정만 완료하면 된다.
2번부터 차례대로 설정을 해보자.
다음과 같이 configurations을 사용하면 컴파일 에러가 발생한다.
configurations {
...
asciidoctorExt // 컴파일 에러
}
코틀린 Gradle을 사용하면서 커스텀 configuration을 구성하는 방법이 변경되었는데, 다음과 같이 변수로 만들어서 사용하면 된다.
asciidoctorExt = configurations.create("asciidoctorExt") {
extendsFrom(configurations["testImplementation"])
}
2번을 설정했다면 간단하게 설정할 수 있다.
또한, io.spring.dependency-management
플러그인 덕분에 버전을 굳이 명시할 필요는 없으므로 다음과 같이 설정한다.
dependencies {
...
asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor")
}
마찬가지로, 2번을 설정했다면 다음과 같이 쉽게 적용할 수 있다.
tasks.asciidoctor {
...
configurations("asciidoctorExt")
baseDirFollowsSourceFile() // index.adoc에 별도 adoc 파일을 import 하려면 해당 설정이 필요하다.
...
}
이제 테스트를 수행하면, build/docs/asciidoc
폴더에 HTML 파일이 생성된다.
src/docs/asciidoc
폴더에 asciidoc 파일을 만들어야 한다.
하지만, 이 작업만 한다고 해서 문서를 제공할 수 없으므로 어딘가에 배포하여 제공해야 한다.
따라서 빌드가 되기 전, resources/static
폴더에 HTML 파일을 옮겨 서버에서 문서화 파일을 제공하는 것이 바람직하다.
다음과 같은 설정을 추가하여 작업을 자동화할 수 있다.
tasks.register("copyDocument", Copy::class) {
dependsOn(tasks.asciidoctor)
doFirst {
delete(file("src/main/resources/static/docs"))
}
from(file("build/docs/asciidoc"))
into(file("src/main/resources/static/docs"))
}
tasks.build {
dependsOn(tasks.getByName("copyDocument"))
}
위의 설정을 통해 Spring REST Docs를 사용할 수 있다.
하지만 약간의 아쉬운 점은 Kotlin을 사용하므로 컴파일 시점에 에러를 잡을 수 있다는 것이 기존 Groovy를 사용했을 때와 차이점이라 했는데, 지금은 문자열을 통해 작업하고 있는 것이 많으므로 해당 이점을 누릴 수 없다.
configurations("asciidoctorExt")
...
dependsOn(tasks.getByName("copyDocument"))
이것을 변수를 사용하여 효과적으로 설정을 변경해보자.
커스텀 Task를 생성할 때 굳이 클래스를 매개변수로 넣을 필요가 없다.
제네릭을 사용하면 더 직관적으로 설정할 수 있다.
tasks.register<Copy>("copyDocument") {
dependsOn(tasks.asciidoctor)
doFirst {
delete(file("src/main/resources/static/docs"))
}
from(file("build/docs/asciidoc"))
into(file("src/main/resources/static/docs"))
}
dependsOn 설정은 Task를 실행하기 전, 특정 Task를 실행하고 성공한 다음에 Task를 실행시키게 할 수 있는 기능이다.
Kotlin을 사용하면 dependsOn 설정은 메서드이다. 따라서 변수를 넣는다.
기존에는 다음과 같이 Task를 변수로 주입한다.
tasks.build {
dependsOn(tasks.getByName("copyDocument"))
}
여기서 getByName()
메서드는 문자열로 Task를 찾아오는 메서드이다.
따라서 컴파일 시점에 에러를 잡을 수 없다.
그런데 이미 위에서 copyDocument
Task를 선언했다.
즉, 해당 Task를 변수로 받으면, 굳이 getByName()
메서드를 통해 Task를 가져올 필요가 없다.
tasks.register<>()
메서드는 TaskProvider
를 반환한다.
따라서 다음과 같이 설정할 수 있다.
val copyDocument = tasks.register<Copy>("copyDocument") {
dependsOn(tasks.asciidoctor)
doFirst {
delete(file("src/main/resources/static/docs"))
}
from(file("build/docs/asciidoc"))
into(file("src/main/resources/static/docs"))
}
...
tasks.build {
dependsOn(copyDocument)
}
그렇다면 마찬가지로, asciidoctorExt configurations 설정 또한 변수를 주입할 수 있다.
// configurations.create() 메서드의 반환 타입은 Configuration이다.
val asciidoctorExt = configurations.create("asciidoctorExt") {
// 문자열을 사용하지 않고, 프로퍼티로 접근할 수 있다.
// extendsFrom(configurations["testImplementation"])
extendsFrom(configurations.testImplementation.get())
}
...
tasks.asciidoctor {
...
configurations(asciidoctorExt)
...
}
하지만, 해당 설정을 한 뒤, 빌드를 하면 다음과 같은 예외가 발생한다.
Could not determine the dependencies of task ':asciidoctor'.
Configuration with name '/Users/...'
configurations()
메서드에는 다음과 같이 Javadoc으로 나와있다.
/** Add additional configurations.
*
* @param configs Instances of {@link Configuration} or anything convertible to a string than can be used
* as a name of a runConfiguration.
*/
void configurations(Object... configs) {
this.asciidocConfigurations.addAll(configs)
}
즉, Configuration
타입의 인스턴스이면 된다고 한다.
하지만 어떠한 이유인지, 위의 예외가 발생하였고 결국 다음과 같은 설정을 하였다.
val asciidoctorExt = "asciidoctorExt"
configurations.create(asciidoctorExt) {
extendsFrom(configurations.testImplementation.get())
}
...
tasks.asciidoctor {
...
configurations(asciidoctorExt)
...
}
기존 Groovy를 사용하던 것과 다르게 Kotlin을 사용하여 Gradle 설정을 하면, 컴파일 시점에 오타로 인한 에러를 잡을 수 있다.
또한 익숙한 Kotlin 문법을 사용하기에 설정 또한 직관적으로 할 수 있다.
스프링 공식 문서에서 REST Docs 설정이 Groovy를 기준으로 설명되어 있기에 Kotlin 문법으로 변경할 때 약간의 어려운 점이 있지만, 익숙해진다면 오히려 더 직관적인 설정이 가능하다.