[Spring Boot] Multi Module

Spring Boot

Language :

Multi-module single project

Organize projects independent of each other into modules and combine them into one project.

Modules

Independent components that exist within a large system

The system constituting the program is separated into functional units and made independent.

Responsibilities and roles are clear.(Single Liability Principle)

It has minimal dependence.(Prevent unnecessary movements and spaghetti codes)

Multi-module

Package collection

Groups that can reuse associated packages and resources

Advantages

Reusable

  • Ensure the identity of the domain structure and rules.
  • Required features can be taken from other modules and used.
  • Minimize duplicate code.

Increase productivity

  • Since the functions are separated for each module, it is possible to predict the extent to which the functions affect.
  • No understanding of the entire project is required.
  • There is no need to know which controller and service the domain model leads to.
  • When a bug or change occurs, build redistribute only the corresponding module without the need to build the entire project.(Reduce build time)

Minimize dependency

  • Since the degree of coupling is low, the influence of the change may be minimized.

Easy to change structure

  • When you change the structure to MSA (Microservice Architecture), you just have to make each module a separate server.
  • It is easy to change to Monolithic Architecture.

Accessibility

  • You don't have to run IDE on a project-by-project basis.

Weakness

Configuration file and package dependency management are required.

  • Care should be taken in the use of common modules.
  • The application may become heavy due to unnecessary dependence in the reference module.

기본 구조

root project
├── app
│    └── src
│          └── main
│                ├── kotlin
│                │   └── com.dev.app
│                │       ├── AppApplication.java
│                │       ├── controller
│                │       │   └── UserController.java
│                │       └── service
│                │            └── UserService.java
│                └── resources
│                      └── application.yml
├── batch
│    └── src
│          └── main
│                ├── kotlin
│                │   └── com.dev.batch
│                │       ├── BatchApplication.java
│                │       └── batchjob
│                │           └── SimpleBatchJob.java
│                └── resources
│                      └── application.yml
└── domain
      └── src
            └── main
                  ├── kotlin
                  │   └── com.dev.domain
                  │       ├── domain
                  │       │   └── User.java
                  │       └── repository
                  │            └── UserRepository.java
                  └── resources
                        └── application.yml

Organize Client, App, and Batch into modules that were divided into projects

root project
├── app
├── client
├── batch
└── domain

Example

1. Create Root Project (Spring Initializr, Kotlin, Gradle)

[root] build.gradle

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.7.5" apply false
    id("io.spring.dependency-management") version "1.0.15.RELEASE" apply false

    kotlin("jvm")
    kotlin("plugin.spring") apply false
    kotlin("plugin.allopen") apply false
    kotlin("plugin.noarg") apply false
    kotlin("kapt") apply false
}

allprojects {
    group = "com.example"
    version = "0.0.1-SNAPSHOT"

    repositories {
        mavenCentral()
    }
}

subprojects {
    apply {
        plugin("java")
        plugin("kotlin")
        plugin("kotlin-jpa")
        plugin("org.springframework.boot")
        plugin("io.spring.dependency-management")
    }

    dependencies {
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    }

    configure<JavaPluginExtension> {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    tasks.withType<KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = JavaVersion.VERSION_17.toString()
        }
    }

    tasks.withType<Test> {
        useJUnitPlatform()
    }
}

project(":dev-domain") {
    apply(plugin = "kotlin-spring")
    apply(plugin = "kotlin-kapt")
}

project(":dev-app") {
    apply(plugin = "kotlin-spring")
    dependencies {
        implementation(project(":dev-domain"))
    }
}
  • pugins: set apply false so that plugins are not applied immediately to the subproject
  • allprojects: apply to all projects including rootProject
  • subprojects: Apply to subprojects included in setting.gradle
  • project: Setting dependencies for specific subprojects included in setting.gradle
  • Sometimes build.gradle uses compile(v6) and api(v7) to inherit the dependence of the module, which is not recommended because it increases the dependence.

[root] gradle.properties

kotlinPluginVersion=1.6.21

[root] settings.gradle

pluginManagement {
    val kotlinPluginVersion: String by settings
    plugins {
        kotlin("jvm") version kotlinPluginVersion
        kotlin("plugin.spring") version kotlinPluginVersion
        kotlin("plugin.allopen") version kotlinPluginVersion
        kotlin("plugin.noarg") version kotlinPluginVersion
        kotlin("kapt") version kotlinPluginVersion
    }
}

rootProject.name = "multi-module"

2. Deleting unnecessary src folders

Root projects are responsible for managing modules

3. Create Domain Module

When creating a Domain Module, you need to think about whether to have only JpaRepository or QueryDsl.

  • JpaRepository only: I need an intermediate layer module that connects to each apiYou can have minimal dependency.
  • Create JpaRepository + QueryDsl: queries that depend on a particular module are brought together, causing the Domain Module to become bloated.

If multiple infrastructures are used (e.g., RDBMS + Redis), they can be written as independent modules to keep the principle of single responsibility, and a Domain Service Module with independent modules is created and used.

[dev-domain] build.gradle

description = "Domain Module"

allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.Embeddable")
    annotation("javax.persistence.MappedSuperclass")
}

noArg {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.Embeddable")
    annotation("javax.persistence.MappedSuperclass")
}

dependencies {
    implementation("org.springframework.boot:spring-boot-configuration-processor")
    implementation("com.querydsl:querydsl-jpa")
    kapt("com.querydsl:querydsl-apt::jpa")

    implementation("org.springframework.boot:spring-boot-starter-jdbc")
    implementation("mysql:mysql-connector-java")
}

tasks.getByName<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    enabled = false
}

tasks.getByName<Jar>("jar") {
    enabled = true
}

Modules that do not require Main Class are set to generate only plain jar that does not contain dependencies.

4. Create App Module

[dev-app] build.gradle

description = "App Module"

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

[dev-app] Main Class

Set the entry and repository bean on the Domain Module to be found because the parent package was created differently for each module.

  • Method 1: Using @ComponentScan
@SpringBootApplication
@ComponentScan(basePackages = ["com.dev.domain"])
class DevAppApplication
  • Method 2: @EntityScan, @EnableJpaRepositories. You must write a package twice.
@SpringBootApplication
@EntityScan(basePackages = ["com.dev"])
@EnableJpaRepositories(basePackages = ["com.dev"])
class DevAppApplication

5. Confirm submodule

[root] settings.gradle

...

rootProject.name = "multi-module"
include("dev-domain")
include("dev-app")

The module is automatically set.

6. Module-specified build

Submodules are also built.

$ ./gradlew :dev-app:build -x test

Reference

https://techblog.woowahan.com/2637/

https://cjw-awdsd.tistory.com/55

https://kotlinworld.com/324

https://jinhanchoi1.medium.com/spring-boot-gradle-plugin-multi-module-build-bootjar-false-5e53a1f6224c

https://hyeon9mak.github.io/woowahan-multi-module/

https://do-study.tistory.com/122

https://kotlinworld.com/317

https://bokyung.dev/2022/03/23/kotlin-gradle-querydsl/

https://velog.io/@cha-sung-soo/Multi-Module-사용-이유

https://wildeveloperetrain.tistory.com/183

https://www.inflearn.com/questions/636766/멀티-모듈에-대한-질문이-있습니다

민갤

Back-End Developer

백엔드 개발자입니다.