[Spring Boot] Multi Module

Spring Boot

Language :

멀티 모듈 단일 프로젝트

서로 독립적인 프로젝트를 모듈로 구성하여 프로젝트 하나로 묶는다.

모듈

큰 체계 안에 존재하는 독립적인 구성요소

프로그램을 구성하는 시스템을 기능 단위로 분리하여 독립시킨다.

책임과 역할이 명확하다. (단일 책임 원칙)

의존성을 최소한만 가진다. (불필요한 동작과 스파게티 코드 예방)

멀티 모듈

패키지 집합체

관련된 패키지와 리소스들을 재사용할 수 있는 그룹

장점

재사용성

  • 도메인 구조와 규칙에 대한 동일성을 보장한다.
  • 필요한 기능을 다른 모듈에서 가져다 사용할 수 있다.
  • 중복 코드를 최소화한다.

생상성 향상

  • 모듈별로 기능을 분리하기 때문에 기능이 영향을 끼치는 범위를 예측할 수 있다.
  • 전체 프로젝트에 대한 이해도가 필요 없다.
  • 도메인 모델이 어떤 Controller, Service로 이어지는 지 알 필요 없다.
  • 버그나 변경이 발생했을 때 프로젝트 전체를 빌드할 필요없이 해당 모듈만 빌드 재배포한다. (빌드 시간 단축)

의존성 최소화

  • 결합도가 낮아서 변경에 따른 영향력을 최소화할 수 있다.

구조 변경 용이

  • MSA(Microservice Architecture)로 구조 변경 시 각 모듈을 개별 서버로 만들기만 하면 된다.
  • Monolithic Architecture로 변경이 쉽다.

접근성

  • IDE를 프로젝트마다 실행하지 않아도 된다

단점

설정 파일과 패키지 의존성 관리가 필요하다.

  • 공통 모듈 사용에 주의해야 한다.
  • 참조하는 모듈에서 불필요한 의존성으로 인해 어플리케이션이 무거워질 수 있다.

기본 구조

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

프로젝트로 나뉘어 있던 Client, App, Batch를 모듈로 구성

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

예제

1. Root 프로젝트 생성 (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: subproject에 plugin이 바로 적용되지 않게 apply false 설정
  • allprojects: rootProject를 포함한 모든 프로젝트에 적용
  • subprojects: setting.gradle 에 include 된 하위 프로젝트에 적용
  • project: setting.gradle 에 include 된 특정 하위 프로젝트에 의존성 설정
  • 간혹 build.gradle 에서 compile(v6), api(v7)를 사용하여 모듈이 가진 의존성을 상속시키는 경우가 있는데 이는 의존성을 높이기 때문에 권장하지 않는다.

[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. 불필요한 src 폴더 삭제

루트 프로젝트는 모듈을 관리하는 역할

3. Domain Module 생성

Domain Module을 작성할 때 JpaRepository만 갖게 할지, QueryDsl도 같이 갖고 있게 할지에 대한 고민이 필요하다.

  • JpaRepository만 작성: 각 Api와 연결되는 중간 계층 모듈이 필요하다. 의존성을 최소한만 가질 수 있다.
  • JpaRepository + QueryDsl 작성: 특정 모듈에 의존적인 쿼리가 하나로 모이게 되어 Domain Module이 비대해진다.

여러 인프라를 사용한다면(예시: RDBMS + Redis) 각각 독립된 모듈로 작성하여 단일 책임 원칙을 지킬 수 있도록 하고, 독립된 모듈들을 품은 중간 계층 모듈(Domain Service Module)을 작성해서 사용하도록 한다.

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

Main Class가 필요없는 모듈은 의존성이 포함되지 않는 plain jar 만 생성하도록 설정한다.

4. 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

Module마다 상위 패키지를 다르게 작성했기 때문에 Domain Module 에 있는 entity와 repository bean을 찾을 수 있도록 설정한다.

  • 방법 1: @ComponentScan 사용
@SpringBootApplication
@ComponentScan(basePackages = ["com.dev.domain"])
class DevAppApplication
  • 방법 2: @EntityScan, @EnableJpaRepositories 를 사용. package를 두 번 작성해야 한다
@SpringBootApplication
@EntityScan(basePackages = ["com.dev"])
@EnableJpaRepositories(basePackages = ["com.dev"])
class DevAppApplication

5. 하위 모듈 확인

[root] settings.gradle

...

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

자동으로 모듈이 설정된다.

6. 모듈 지정 빌드

하위 모듈도 같이 build 된다.

$ ./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

백엔드 개발자입니다.