ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin DSL Gradle 멀티 모듈 적용
    프로젝트/자프링 -> 코프링 마이그레이션 2022. 12. 27. 00:01
    728x90

    기존 프로젝트의 패키지 구조

    그림1

     

    기존 프로젝트는 [그림 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

     

    GitHub - Junuu/Refactoring-Java-To-Kotlin: 로그인 + 게시글로 구성된 자바 프로젝를 리팩토링 및 기능 고도

    로그인 + 게시글로 구성된 자바 프로젝를 리팩토링 및 기능 고도화. Contribute to Junuu/Refactoring-Java-To-Kotlin development by creating an account on GitHub.

    github.com

    추후 코드 작업에 따라 조금 구성이 바뀔 순 있지만 전체적인 흐름은 비슷할겁니다.

     

     

    Layer 분리하기

    부족하지만 다음과 같은 Layer를 만들어보고자 했습니다

    현재는 Domain만 재사용할 수 있을 것 같고 Common까지 모듈을 나누어 고도화 할 수 있을것 같습니다.

    https://mesh.dev/20210910-dev-notes-007-hexagonal-architecture/

     

     

     

    참고자료

    https://junuuu.tistory.com/490

     

    우아한멀티모듈 by 권용근님

    우아한형제들 권용근님의 우아한멀티모듈을 보고 요약한 내용입니다. https://www.youtube.com/watch?v=nH382BcycHc 멀티 모듈 프로젝트의 등장 배경 회원 시스템을 개발한다고 하면 다음과 같은 독립된 프

    junuuu.tistory.com

    https://namocom.tistory.com/986

     

    Kotlin DSL Gradle: 멀티 모듈로 변경하기

    단일 모듈 기반의 프로젝트 spring initializr 에서 프로젝트를 만들면 기본적으로 단일 모듈 기반의 프로젝트를 생성한다. 이 구조는 단순하고 만들기 쉽지만 단점도 있다. 핵심 비즈니스 구현을 하

    namocom.tistory.com

    https://kotlindays.com/2019/12/06/multi-module-spring-boot-in-kotlin-dsl/index.html

     

    How to Create a Multi-Module Spring Boot Project using Gradle’s Kotlin DSL

    An example of using the Gradle Kotlin DSL for a multi-module Spring project

    kotlindays.com

    https://tecoble.techcourse.co.kr/post/2021-09-06-multi-module/

     

    멀티 모듈 적용하기 with Gradle

    이번 글에서는 프로젝트를 구성하는 데 있어 멀티 모듈 활용했을 때의 장점과 간단한 설정 방법을 알아본다. 멀티 모듈의 개념을 처음 접하는 사람들이 읽어보기를 추천한다.

    tecoble.techcourse.co.kr

    https://www.youtube.com/watch?v=4dO2Wa2fAYI&t=56s 

     

    댓글

Designed by Tistory.