서비스의 규모가 커지고 기능이 다양해지면 코드는 점점 복잡해지기 마련이죠. 신규 기능 추가 및 유지보수를 원활하게 진행하기 위해서는 기능별, 역할별로 모듈을 분리하고 이들의 의존성을 체계적으로 관리하는 것이 좋습니다. 공통 기능을 별도의 모듈을 분리하여 다른 모듈에서 이를 같이 사용함으로써 재사용성과 일관성을 크게 향상시킵니다. 또한 각 모듈을 독립적으로 개발할 수 있기 때문에 생산성 측면의 개선도 기대할 수 있죠.
root/
├── build.gradle.kts
├── settings.gradle.kts
├── api/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── kotlin/
│ │ └── io.cloudtype.api
│ │ ├── controller
│ │ ├── service
│ │ └── ApiApplication.kt
│ └── resources/
├── core/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── kotlin/
│ │ └── io.cloudtype.core
│ │ ├── config
│ │ ├── model
│ │ └── repository
│ └── resources/
└── gradle/
└── wrapper/
├── gradle-wrapper.jar
└── gradle-wrapper.properties
이 프로젝트는 controller와 service를 담당하는 api와 model, repository를 담당하는 core의 두 개 모듈로 구성되어 있습니다. 각 모듈 내부의 디렉토리 및 파일 구조를 살펴보면 마치 독자적인 Spring Boot 프로젝트를 이루는 것 같은 모습인데요, 자세히 살펴보면 그 내용과 구성에 약간의 차이가 있습니다.
루트 디렉토리와 각 모듈 내부에는 각각 build.gradle.kts
와 settings.gradle.kts
이 있으며, 모듈 간에 필요한 패키지를 올바르게 참조하고 이를 jar 형태로 패키징하기 위해서 아주 중요한 역할을 합니다.
/settings.gradle.kts
...
include("core", "api")
루트 위치에서 각 모듈의 settings.gradle.kts
에 정의된 rootProject.name
을 include()
의 인수로 넘깁니다.
/build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.2.8" apply false
id("io.spring.dependency-management") version "1.1.6"
kotlin("plugin.jpa") version "1.9.24"
kotlin("jvm") version "1.9.24"
kotlin("plugin.spring") version "1.9.24" apply false
}
allprojects {
group = "com.example"
version = "0.0.1-SNAPSHOT"
repositories {
mavenCentral()
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
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")
java {
sourceCompatibility = JavaVersion.VERSION_17
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
dependencies {
implementation(kotlin("script-runtime"))
}
subprojects
에서 각 모듈에 적용될 항목을 명시합니다.
core/build.gradle.kts
plugins {
kotlin("plugin.spring") version "1.9.24"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
}
tasks.bootJar {
enabled = false
}
tasks.jar {
enabled = true
}
tasks.test {
enabled = false
}
api/build.gradle.kts
plugins {
id("org.springframework.boot")
kotlin("plugin.spring")
}
springBoot {
mainClass.set("io.cloudtype.api.ApiApplicationKt")
}
dependencies {
implementation(project(":core"))
implementation("org.springframework.boot:spring-boot-starter-web")
runtimeOnly("com.h2database:h2")
}
tasks.test {
enabled = false
}
mainClass.set()
의 인수로 진입점인 main 함수가 포함된 클래스를 넘깁니다. Kotlin으로 코드를 작성한 경우, 파일명.kt
를 파일명Kt
라고 작성해야 합니다.core
모듈에 대한 의존성을 적용하기 위해 dependencies
에 implementation(project(":core"))
를 명시합니다.실습은 아래의 Spring Boot 어플리케이션을 통해 진행됩니다. 저장소를 clone 하거나 fork 해주세요.
클라우드타입의 프로젝트 페이지에서 ➕ 버튼을 누르고 Spring Boot를 선택한 후, 미리 fork 해놓은 springboot-multi-module 를 선택합니다.
멀티 모듈 프로젝트의 경우, 개발자가 gradle 세팅에서 main 클래스로 지정한 모듈의 executable jar 파일을 실행해야 하므로 시작 명령어에 해당 jar 파일의 경로를 정확히 명시해야 정상적으로 어플리케이션이 실행됩니다.
기타 설정은 아래를 참고하여 입력한 후 배포하기 버튼을 클릭합니다.
java -jar api/build/libs/api-0.0.1-SNAPSHOT.jar
배포가 완료되면 접속하기 버튼을 누르고 주소창에 /api/users
경로를 추가하여 접속한 후 초기 데이터가 조회되는지 확인합니다.