멀티 모듈 프로젝트 구성하기

TunaHG·2024년 7월 27일

새로운 스프린트를 시작하며, 메시지를 주고받을 수 있는 Producer, Consumer 기능을 프로젝트에 붙여넣어야 되는 상황이 생겼다.
기존 프로젝트에 바로 붙여넣을 수도 있겠지만, 기존 API를 제공하고 있는 모듈과는 별개로 모듈을 하나 새로 생성해서 멅티모듈로 프로젝트를 전환하는게 어떻겠냐는 의견이 나왔다.
그럼 멀티 모듈 프로젝트로 전환하는 김에 기존 Java였던 프로젝트를 Kotlin으로도 변경하면서 아예 새로운 프로젝트를 만들어 버리자! 라고 의견이 발전되서... 멀티 모듈 프로젝트를 새로 만들어 보기로 했다.

기존에 멀티 모듈로 작성된 프로젝트가 몇 개 있어서 해당 프로젝트를 그대로 가져와서 만들면 금방 만들 수 있었겠지만, 새로운 프로젝트에서는 사용하지 않을 의존성이나 Class들이 많이 존재하다보니 정리하는게 더 힘들겠다는 생각과 완전 기초부터 멀티 모듈로 만들어보면 그 경험이 오랫동안 기억에 남지 않을까 싶은 생각으로 완전 밑바닥부터 새로만들어 보기로 했다.

기초는 SpringBoot 프로젝트로 만들고, src 폴더를 삭제한다.
그리고 루트 모듈에 새로운 모듈을 Spring Boot로 추가한다.
그리고 각 Spring Boot 모듈에서 src 폴더와 build.gradle.kts를 제외한 모든 파일을 제거한다.
(이외의 설정들은 모두 루트 모듈에서 진행할 예정이므로)
그럼 아래 이미지와 같은 형태가 된다.

presentation, infrastructure 모듈은 모듈 내에 또 멀티모듈로 진행했다.

settings.gradle.kts

여기서 이제 settings.gradle.kts를 먼저 수정해본다.
각 모듈을 gradle에 포함될 수 있도록 include()를 선언해줘야 한다.

rootProject.name = "root"
include("presentation:api")
include("presentation:stream")
include("application")
include("infrastructure:db")
include("infrastructure:http")

gradle.properties

혹시 루트 모듈에 Spring Boot 프로젝트로 만들면서 gradle.properties 파일이 없었다면 추가해준다.

kotlin.code.style=official

gradle.properties를 추가한 이유는 바로 아래 build.gradle.kts를 설명할때 나온다.

root build.gradle.kts

이제 루트 모듈의 build.gradle.kts를 어떻게 작성하는지 살펴본다.

plugins

build.gradle.kts의 최상단에 선언할 plugins부터 살펴본다.

plugins {
    id("org.springframework.boot") version "3.3.2" apply false
    id("io.spring.dependency-management") version "1.1.6" apply true

    val kotlinVersion = "1.9.24"
    kotlin("jvm") version kotlinVersion apply true
    kotlin("plugin.spring") version kotlinVersion apply false
    kotlin("plugin.jpa") version kotlinVersion apply false
    kotlin("kapt") version kotlinVersion apply false
}

plugins에는 말 그대로 해당 프로젝트에서 사용할 plugin들을 명시한다.
Spring Boot 프로젝트에서 사용할 plugin부터 kotlin으로 spring을 개발하기 위한 plugin들까지 선언해준다.

apply는 어떤 것인지 궁금한데, 링크를 보면 gradle plugins에 대해 자세히 알 수 있다.
간단하게 설명하면, 플러그인을 사용하기 위해서는 두 단계가 필요한데 플러그인을 귀결(resolve)하는 것과 적용(apply)하는 것이 있다.
귀결(resolve)하는 것은 플러그인을 포함하는 올바른 버전의 jar를 찾아 스크립트 클래스 경로에 추가하는 것을 의미하고, 적용(apply)하는 것은 플러그인을 사용하고자 하는 프로젝트에서 플러그인의 Plugin.apply(T)를 실행하는 것을 의미한다.

다중 프로젝트 빌드가 있는 경우 빌드의 일부 또는 전체 하위 프로젝트에 플러그인을 적용하고 싶지만 루트 프로젝트에서는 적용하고 싶지 않을 경우가 있으니, apply false를 사용하여 현재 프로젝트에서 플러그인을 적용하지 않도록 gradle에 지시한 다음 하위 프로젝트의 빌드 스크립트에서 버전 없이 블록을 사용할 수 있다.

그래서 나는 루트 프로젝트에서 사용할 id("io.spring.dependency-management")와 무조건 apply true로 선언해야 하는 kotlin("jvm")true로 선언했다.

allprojects, subprojects

다음은 allprojects와 subprojects인데, 둘이 어떤 차이점을 가지고 있는지 부터 알아본다.
간단하게, allprojects는 루트 모듈을 포함한 모든 모듈에 적용할 gradle 구문이며 subprojects는 루트 모듈을 제외한 모든 하위 모듈에 적용할 gradle 구문이다.

allprojects

allprojects에는 프로젝트 그룹 및 버전을 명시하고, 모든 모듈들이 mavenCentral()을 사용해서 dependency들을 가져올 예정이므로 선언해준다.

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

    repositories {
        mavenCentral()
    }
}

subprojects

subprojects에는 각 하위 모듈에서 사용할 dependency 및 gradle 구문을 설정한다.

subprojects {
    apply {
        plugin("io.spring.dependency-management")
    }

    dependencyManagement {
        imports {
            mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.3")
        }
        dependencies {
            dependency("com.github.loki4j:loki-logback-appender:1.5.2")
        }
    }

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

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

자세한 내용을 짚어보면,

apply {}를 통해 plugin("io.spring.dependency-management")를 적용해준 것은 dependencyManagement 구문을 작성하기 위해 필요한 내용이다.
기존에 build.gradle에서 사용하던 것과 같이 dependencies 구문과 내부 implementation()을 사용하기 위해서는 plugin("kotlin")apply 해줘야 하는데, 앞서 gradle.properties을 생성했다면 kotlin 플러그인을 apply 해주지 않아도 사용할 수 있다.

dependencyManagement는 링크를 보면 자세히 확인할 수 있다.
프로젝트 단위로 의존성을 관리하기 위해 bom을 사용하는 경우 implementation platform을 사용하거나 위처럼 imports를 사용한다.
그리고 dependency를 설정하면 하위 프로젝트에서는 버전을 명시하지 않고 사용할 수 있도록 subprojects에서 버전을 명시해서 전역적으로 버전을 관리할 수 있다.

전체적인 build.gradle.kts를 살펴보면 아래와 같다.

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

plugins {
    id("org.springframework.boot") version "3.3.2" apply false
    id("io.spring.dependency-management") version "1.1.6" apply true

    val kotlinVersion = "1.9.24"
    kotlin("jvm") version kotlinVersion apply true
    kotlin("plugin.spring") version kotlinVersion apply false
    kotlin("plugin.jpa") version kotlinVersion apply false
    kotlin("kapt") version kotlinVersion apply false
}

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

    repositories {
        mavenCentral()
    }
}

subprojects {
    apply {
        plugin("io.spring.dependency-management")
    }

    dependencyManagement {
        imports {
            mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.3")
        }
        dependencies {
            dependency("com.github.loki4j:loki-logback-appender:1.5.2")
        }
    }

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

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

api build.gradle.kts

다음은 presentation.api 모듈의 build.gradle.kts를 살펴본다.
각 모듈 내의 build.gradle.kts에서는 각 모듈 간의 의존성을 설정한다.

plugins

플러그인은 루트 모듈에서 버전을 모두 명시해줬으므로, 해당 모듈에서 사용할 플러그인만 버전 없이 명시하면 된다.

plugins {
    id("org.springframework.boot")
    id("io.spring.dependency-management")

    kotlin("jvm")
    kotlin("plugin.spring")
}

dependencies

그리고 dependencies를 설정해준다.
해당 모듈에서 필요한 의존성을 선언해준다. 각 모듈간의 의존성도 여기에서 설정한다.

dependencies {
    implementation(project(":application"))
    implementation(project(":infrastructure:db"))
    implementation(project(":infrastructure:http"))

    implementation("org.springframework.boot:spring-boot-starter-web")
}

각 모듈간 의존성 및 아키텍처 구성에 대해서는 다음에 자세히 살펴본다.
우선은 해당 모듈에서 사용할 다른 모듈을 선언하기 위해서는 위처럼 project(":module")로 가져온다는 것만 인지한다.

tasks

다음은 plain.jar 파일을 생성하지 않기 위해 tasks를 설정한다.

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

bootJar를 통해 .jar 파일이 생성되게 되는데, Jar task를 통해 plain.jar 파일이 생성된다.
해당 프로젝트에서는 plain.jar파일이 필요하지 않아서 빌드시 생성되지 않게 하기 위해 선언했다.

결과적으로 api 모듈의 build.gradle.kts는 다음과 같다.

plugins {
    id("org.springframework.boot")
    id("io.spring.dependency-management")

    kotlin("jvm")
    kotlin("plugin.spring")
}

dependencies {
    implementation(project(":application"))
    implementation(project(":infrastructure:db"))
    implementation(project(":infrastructure:http"))

    implementation("org.springframework.boot:spring-boot-starter-web")
}

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

api 모듈의 build.gradle.kts를 참고하여 다른 모듈의 build.gradle.kts를 각각 설정해준다.
내가 작성하면서 신경썼던 점은 확실히 각 모듈에 필요한 의존성만 설정하는 것이였다.
혹은 presentation 모듈 내의 api, stream 모듈에서 모두 필요한 의존성은 presentation 모듈의 build.gradle.kts에 선언하는 방법도 있을 것 같다.

정리

멀티 모듈을 진행하며 좋다고 생각한 점은 의존성을 주입하지 않은 다른 모듈에 존재하는 객체는 아예 가져다가 사용할 수 없도록 방법 자체가 막힌다는 점이다.
그렇게 개발하지 않도록 팀 내에서 합의가 진행되었다고 해도 시간이 지나거나 새로운 팀원이 들어오는 등 여러 변수로 인해 개발이 가능하다면 그렇게 개발할 수도 있기 때문에 애초에 여지를 주지 않는 것이 컨벤션, 아키텍처를 지키기 위한 가장 좋은 방법이라고 생각한다.

다음은 앞서 말했던 api모듈에서 왜 다른 모듈의 의존성을 저렇게 가져왔는지, 프로젝트 구조를 어떻게 구성할 생각인 건지에 대해서 새로운 포스팅을 작성해보겠다.

profile
나태해지지 말자

0개의 댓글