Kotlin Gradle을 사용하여 Spring REST Docs 적용하기

Glen·2023년 12월 11일
1

배운것

목록 보기
28/37

서론

최근 Gradle에서는 코틀린 문법을 사용한 버전을 기본 언어로 채택하였다.
따라서 코틀린 + 스프링 프로젝트를 사용하면서, Gradle 설정도 Groovy를 사용하는 것이 아닌, 코틀린 문법으로 된 Gradle 설정을 사용하였다.

미래의 변화에 유연하게 대응하기 위함도 있고, 지금 진행 중인 스프링 프로젝트도 코틀린을 사용하므로 그대로 사용해 보기로 했다.

하지만 시작부터 난감하게 프로젝트를 빌드하니 에러를 마주했다. 😂

기존 groovy 문법을 사용했을 때 어떤 점에서 이득인지 간단하게 살펴보고, 기존 설정을 어떻게 바꿔야 하는지 알아보겠다.

본론

Kotlin을 사용했을 때 차이점

기존 groovy 문법을 사용했을 때의 가장 큰 차이점은 컴파일 시점에 오류를 검사할 수 있다.

tasks.withType<Test> {  
    useJUnitPlatformz() // 오타를 컴파일 에러로 알려준다!
}

또한 코틀린 문법을 사용하기 때문에 IntelliJ를 사용한다면, 자동 완성 기능을 지원받을 수 있다!

그 외 간소화된 플러그인 구문 등이 있지만, 크게 체감은 되지 않았다.

초기 build.gradle.kts 설정

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)
}

기본 Spring REST Docs 설정

스프링 공식 문서에는 다음과 같이 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번부터 차례대로 설정을 해보자.

2. configurations 설정

다음과 같이 configurations을 사용하면 컴파일 에러가 발생한다.

configurations {
    ...
	asciidoctorExt // 컴파일 에러
}

코틀린 Gradle을 사용하면서 커스텀 configuration을 구성하는 방법이 변경되었는데, 다음과 같이 변수로 만들어서 사용하면 된다.

asciidoctorExt = configurations.create("asciidoctorExt") {  
    extendsFrom(configurations["testImplementation"]) 
}

3. asciidoctorExt 의존성 설정

2번을 설정했다면 간단하게 설정할 수 있다.
또한, io.spring.dependency-management 플러그인 덕분에 버전을 굳이 명시할 필요는 없으므로 다음과 같이 설정한다.

dependencies {
    ...
    asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor")
}

9. configurations 적용

마찬가지로, 2번을 설정했다면 다음과 같이 쉽게 적용할 수 있다.

tasks.asciidoctor {  
    ...
    configurations("asciidoctorExt")  
    baseDirFollowsSourceFile() // index.adoc에 별도 adoc 파일을 import 하려면 해당 설정이 필요하다.
    ... 
}

추가 Spring REST Docs 설정

이제 테스트를 수행하면, 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"))  
}

Kotlin Gradle의 장점을 살려서 효과적으로 적용하기

위의 설정을 통해 Spring REST Docs를 사용할 수 있다.

하지만 약간의 아쉬운 점은 Kotlin을 사용하므로 컴파일 시점에 에러를 잡을 수 있다는 것이 기존 Groovy를 사용했을 때와 차이점이라 했는데, 지금은 문자열을 통해 작업하고 있는 것이 많으므로 해당 이점을 누릴 수 없다.

configurations("asciidoctorExt") 
...
dependsOn(tasks.getByName("copyDocument"))

이것을 변수를 사용하여 효과적으로 설정을 변경해보자.

커스텀 Task 생성

커스텀 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 설정

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 설정

그렇다면 마찬가지로, 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 문법으로 변경할 때 약간의 어려운 점이 있지만, 익숙해진다면 오히려 더 직관적인 설정이 가능하다.

참고

profile
꾸준히 성장하고 싶은 사람

0개의 댓글