-
Kotlin DSL Gradle 멀티 모듈 적용프로젝트/자프링 -> 코프링 마이그레이션 2022. 12. 27. 00:01
기존 프로젝트의 패키지 구조
기존 프로젝트는 [그림 1]처럼 하나의 단일 모듈 기반의 프로젝트로 구성되어 있습니다.
build.gradle.kts에서 의존성이 관리되며 main폴더 아래에 모든 코드들이 들어가 있습니다.
이러한 구조를 멀티모듈으로 변환하고자 합니다.
Why 멀티모듈?
현재는 단일 프로젝트이며 외부에 노출되는 external-api들만 존재합니다.
제공되는 기능 예시
- 사용자는 게시글을 쓸 수 있다
- 사용자는 로그인을 할 수 있다
- 사용자는 회원가입을 할 수 있다
이 상황에서 멀티 모듈을 도입하더라도 큰 의미가 없을 수 있습니다.
하지만 만약 내부에서만 사용하는 internal-api 관리자 api가 존재할 경우 이야기가 달라질 수 있습니다.
다른 프로젝트에서 internal-api를 관리한다면 응답 로직, member domain 등을 중복해서 관리해야 합니다.
하지만 멀티 모듈을 구성한다면 이러한 중복을 제거할 수 있습니다.
또한 빌드, 배포 단위를 미리 쪼개 두었기 때문에 추후에 MSA로 전환하는 작업도 용이하게 관리할 수 있을 것 같습니다.
(이 부분은 실제로 도입, 배포까지 거쳐봐야 자세하게 알 것 같습니다)
두 번째로 각 모듈별로 기능을 분리하여 작성하기 때문에 각 모듈의 기능을 파악하기 쉬워지고 코드를 이해하기 쉬워집니다.
단일 모듈에서는 대부분의 자원과 코드에 대해 제약 없이 접근할 수 있지만 멀티 모듈에서는 의존성을 추가해주지 않으면 해당 모듈의 자원과 코드에 접근할 수 없습니다.
따라서 자신의 관심사에 대한 문제를 해결합니다.
멀티 모듈 프로젝트로 전환하기
IntelliJ IDEA, Kotlin, Groovy 기준
우선 루트 프로젝트는 하위 모듈을 관리하는 역할만 수행하도록 합니다.
여기서 루트 프로젝트는 [그림 1]의 Refactoring-Java-To-Kotlin이 루트 프로젝트가 됩니다.
새로운 모듈 만들기(File -> New -> Module)
1. Kotlin DSL build script 체크
2. Java 체크 해제
3. Kotlin/JVM 선택
Module name : member-external-api로 지정
이후 gradle이 자동으로 재로딩을 진행하고 에러에 프로젝트가 문제가 있다고 나옵니다.
이제 프로젝트 구조를 변경하겠습니다.
src 디렉터리 옮기기
프로젝트 바로 아래에 있는 src 디렉터리를 방금 만든 곳으로 이동시킵니다.
member-external-api에 먼저 옮기고 common, core 등을 만들어 분리할 생각입니다.
root 프로젝트 gradle 수정
1. root 프로젝트의 plugins 들은 appliy false로 적용합니다
2. dependencies를 모두 제거합니다.
3. allprojects 블록을 추가하여 하위 모듈에 공통으로 적용할 값들을 설정합니다. ( group, version, tasks.withType)
4. subprojects 블록을 추가하여 하위 모듈들에 대한 공통적인 설정을 해줍니다.
기존 gradle
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.6.7" id("org.asciidoctor.jvm.convert") version "3.3.2" id("io.spring.dependency-management") version "1.0.11.RELEASE" id("org.jetbrains.kotlin.jvm") version "1.6.21" java id("org.jetbrains.kotlin.plugin.spring") version "1.6.21" id("org.jetbrains.kotlin.plugin.jpa") version "1.6.21" } group = "anthill" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_11 val asciidoctorExtensions by configurations.creating repositories { mavenCentral() } dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.junit.jupiter:junit-jupiter-params:5.4.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("mysql:mysql-connector-java") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.projectlombok:lombok:1.18.24") implementation("org.mindrot:jbcrypt:0.4") implementation("io.jsonwebtoken:jjwt:0.9.1") asciidoctorExtensions("org.springframework.restdocs:spring-restdocs-asciidoctor") testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") compileOnly("org.projectlombok:lombok") runtimeOnly("com.h2database:h2") testImplementation("org.springframework.boot:spring-boot-starter-test") } tasks.withType<Test> { useJUnitPlatform() } val snippetsDir by extra { file("build/generated-snippets") } tasks { asciidoctor { dependsOn(test) configurations("asciidoctorExtensions") inputs.dir(snippetsDir) } register<Copy>("copyDocument") { dependsOn(asciidoctor) from(file("build/docs/asciidoc/index.html")) into(file("src/main/resources/static/docs")) } bootJar { dependsOn("copyDocument") } }
크게 나누어보면 다음과 같습니다
- plugins
- dependencies
- tasks
plugins에 apply false를 적용하는 이유는 더 이상 root project에서는 실행할 게 없기 때문입니다.
allprojects에는 프로젝트의 그룹, 버전, jdk 버전 등을 명시합니다
이후 의존성들은 공통적으로 적용할 것들만 subprojects에 추가해줍니다.
이때 적용할 plugin을 apply 해주어야 합니다(위에서 false 처리했기 때문)
변경된 gradle
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.6.7" apply false id("io.spring.dependency-management") version "1.0.11.RELEASE" apply false id("org.jetbrains.kotlin.jvm") version "1.6.21" apply false java id("org.jetbrains.kotlin.plugin.spring") version "1.6.21" apply false id("org.jetbrains.kotlin.plugin.jpa") version "1.6.21" apply false id("org.asciidoctor.jvm.convert") version "3.3.2" apply false } allprojects { group = "anthill" version = "0.0.1-SNAPSHOT" tasks.withType<JavaCompile> { sourceCompatibility = "11" targetCompatibility = "11" } tasks.withType<Test> { useJUnitPlatform() } tasks.withType<KotlinCompile>{ kotlinOptions{ freeCompilerArgs = listOf("-Xjst305=strict") jvmTarget = "11" } } repositories { mavenCentral() } } subprojects{ apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.plugin.spring") apply(plugin = "org.jetbrains.kotlin.plugin.jpa") apply(plugin = "org.springframework.boot") apply(plugin = "io.spring.dependency-management") dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") } }
이제 루트의 build.gradle.kts는 위와 같이 변경됩니다.
member-external-api gradle.kts
plugins { java id("org.jetbrains.kotlin.plugin.jpa") id("org.asciidoctor.jvm.convert") } val asciidoctorExtensions by configurations.creating dependencies { implementation(project(":domain")) implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") //test testImplementation("org.junit.jupiter:junit-jupiter-params:5.4.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1") //encoding implementation("org.mindrot:jbcrypt:0.4") implementation("io.jsonwebtoken:jjwt:0.9.1") //restdocs asciidoctorExtensions("org.springframework.restdocs:spring-restdocs-asciidoctor") testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") } val snippetsDir by extra { file("build/generated-snippets") } tasks { asciidoctor { dependsOn(test) configurations("asciidoctorExtensions") inputs.dir(snippetsDir) } register<Copy>("copyDocument") { dependsOn(asciidoctor) from(file("build/docs/asciidoc/index.html")) into(file("src/main/resources/static/docs")) } bootJar { dependsOn("copyDocument") archiveFileName.set("boot.jar") } }
controller응답에 대한 모듈입니다.
restdocs에 대한 설정을 가지고 있습니다.
의존성 부분이 깔끔해졌습니다.
자세히 보면 위에서 domain 모듈을 의존하고 있습니다.
domain gradle.kts
plugins { id("org.springframework.boot") id("io.spring.dependency-management") kotlin("plugin.allopen") kotlin("plugin.jpa") kotlin("kapt") } allOpen { annotation("org.springframework.stereotype.Service") } dependencies { implementation(project(":infra-rds")) implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test") implementation("org.mindrot:jbcrypt:0.4") implementation("org.springframework.boot:spring-boot-starter-validation") } tasks.getByName("bootJar") { enabled = false } tasks.getByName("jar") { enabled = true }
domain 모듈의 gradle.kts입니다.
entity와 repositroy, service를 담고 있는 모듈이며 실행할 수 없는 모듈이기 때문에 bootJar를 false, jar를 true로 명시합니다.
domain 모듈은 infra-rds를 의존하고 있습니다.
infra-rds gradle.kts
plugins { id("org.springframework.boot") id("io.spring.dependency-management") kotlin("plugin.spring") kotlin("plugin.jpa") } dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("mysql:mysql-connector-java") runtimeOnly("com.h2database:h2") } tasks.getByName("bootJar") { enabled = false } tasks.getByName("jar") { enabled = true }
db에 관련된 설정만 담겨있는 모듈입니다.
전체 코드는 git repository를 참고하세요
https://github.com/Junuu/Refactoring-Java-To-Kotlin
추후 코드 작업에 따라 조금 구성이 바뀔 순 있지만 전체적인 흐름은 비슷할겁니다.
Layer 분리하기
부족하지만 다음과 같은 Layer를 만들어보고자 했습니다
현재는 Domain만 재사용할 수 있을 것 같고 Common까지 모듈을 나누어 고도화 할 수 있을것 같습니다.
참고자료
https://junuuu.tistory.com/490
https://namocom.tistory.com/986
https://kotlindays.com/2019/12/06/multi-module-spring-boot-in-kotlin-dsl/index.html
https://tecoble.techcourse.co.kr/post/2021-09-06-multi-module/
https://www.youtube.com/watch?v=4dO2Wa2fAYI&t=56s
'프로젝트 > 자프링 -> 코프링 마이그레이션' 카테고리의 다른 글
build.gradle to build.gradle.kts (Groovy를 Kotlin으로 마이그레이션) (0) 2022.12.11 @SpringBootApplication 자바[Java] -> 코틀린[Kotlin]으로 변경 (0) 2022.12.06 [kotlin][Mockito][Junit5] any() must not be null 에러 해결 (0) 2022.12.05 Kotlin 각종 어노테이션 사용법(@Index, @Embeddable, @Size) (0) 2022.12.01 Kotlin과 Lombok 컴파일 에러 해결 (0) 2022.11.30