반응형
05-14 05:47
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

[Spring Boot] Mono Repo, Multi Module (with. Gradle Kotlin DSL) 본문

Framework/Spring Boot

[Spring Boot] Mono Repo, Multi Module (with. Gradle Kotlin DSL)

조용한고라니 2022. 2. 20. 17:25
반응형

Intro

개인 프로젝트 정도의 규모라면 하나의 프로젝트 안에 api, web, admin 등이 모두 들어가있어도 커버가 되지만, 규모가 커지면 각각 개별 프로젝트로 나누어야 관리가 될 정도가 된다. 그럼 나눴다고 가정하자. 우리는 4개의 repository가 있다.

  • api
  • web
  • admin-api
  • admin-web

이때 admin에 어떤 기능을 추가해달라고 요청이 왔다. 그래서 admin-api, admin-web을 수정해서 PR(Pull Request)을 올렸다.

다음 요구사항은 새 프로모션이다. api, web, admin 모두 개발을 했고 PR을 올렸다. 하나의 이슈이지만 4번의 PR을 올렸다. 지금은 '이게 어때서?' 라고 생각할 수 있다. 조금 더 극단적으로 가정해서 주문과 정산, 회원이 다시 분리가 되었다.

  • api
  • web
  • admin-api
  • admin-web
  • user
  • settlement
  • order

어쩌면 하나의 이슈를 처리하는데 7번의 PR을 올려야할 수도 있다. 그래서 이를 하나의 Repository로 묶고자 한다.

장점

1. domain을 따로 빼면 각 프로젝트는 domain에 관한 코드를 개별적으로 들고있지 않아도 된다.

2. 하나의 프로젝트(root project)만 내려받고 관리할 수 있다.

Mono Repo, Multi Module

간단하게 시나리오는 다음과 같다.

  • mono-repo
    • api
    • domain

위 구조로 프로젝트를 구성하고, 깃허브에 올리는 것까지 목표로 한다.

Root 생성

인텔리제이에서 새 프로젝트를 생성한다. root module이 될 첫번째는 Spring Initializr로 생성하나, gradle을 사용해도 무방하다.

 

dependencies

하위 모듈 생성

Root가 될 녀석을 만들었으니, 하위 모듈을 2개 생성한다. 

api 모듈

다음은 domain 모듈이다. 엔티티 관련 코드가 들어간다.

domain 모듈

 

여기까지 정상적으로 진행했다면 settings.gradle.kts파일에 다음과 같이 되어있을 것이다. 없다면 추가해주자.

build.gradle.kts

개인적으로 gradle 스크립트를 다루는데서 굉장히 애먹었다. gradle에 대해 잘 모르기도 하고, 그래서 우선 돌아가는 것을 목표로 여기저기 둘러보며 진행했다.

 

[root] build.gradle.kts

먼저 최상단 프로젝트에서 "src"는 사용하지 않으므로 지우도록 하자.

 

AS-IS

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

plugins {
    id("org.springframework.boot") version "2.6.3"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.10"
    kotlin("plugin.spring") version "1.6.10"
    kotlin("plugin.jpa") version "1.6.10"
}

group = "com.gorany"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

repositories {
    mavenCentral()
}

dependencies {
    //spring boot
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    
    //kotlin
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    
    //lombok
    compileOnly("org.projectlombok:lombok")
    annotationProcessor("org.projectlombok:lombok")
    
    //DB connect
    runtimeOnly("com.h2database:h2")
    runtimeOnly("mysql:mysql-connector-java")
    
    //test
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
}

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

tasks.withType<Test> {
    useJUnitPlatform()
}

 

TO-BE

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
    id("org.springframework.boot") version "2.6.3"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.10"
    kotlin("plugin.spring") version "1.6.10" apply false
    kotlin("plugin.jpa") version "1.6.10" apply false
}

java.sourceCompatibility = JavaVersion.VERSION_11

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

    repositories {
        mavenCentral()
    }
}

subprojects {

    apply(plugin = "java")

    apply(plugin = "io.spring.dependency-management")
    apply(plugin = "org.springframework.boot")
    apply(plugin = "org.jetbrains.kotlin.plugin.spring")

    apply(plugin = "kotlin")
    apply(plugin = "kotlin-spring") //all-open
    apply(plugin = "kotlin-jpa")

    dependencies {
        //spring boot
        implementation("org.springframework.boot:spring-boot-starter-data-jpa")
        implementation("org.springframework.boot:spring-boot-starter-security")
        implementation("org.springframework.boot:spring-boot-starter-web")
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
        developmentOnly("org.springframework.boot:spring-boot-devtools")

        //kotlin
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

        //lombok
        compileOnly("org.projectlombok:lombok")
        annotationProcessor("org.projectlombok:lombok")

        //DB connect
        runtimeOnly("com.h2database:h2")
        runtimeOnly("mysql:mysql-connector-java")

        //test
        testImplementation("org.springframework.boot:spring-boot-starter-test")
        testImplementation("org.springframework.security:spring-security-test")
    }

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }

        dependencies {
            dependency("net.logstash.logback:logstash-logback-encoder:6.6")
        }
    }

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

    tasks.withType<Test> {
        useJUnitPlatform()
    }

    configurations {
        compileOnly {
            extendsFrom(configurations.annotationProcessor.get())
        }
    }
}

//api <- domain 의존
project(":api") {
    dependencies {
        implementation(project(":domain"))
    }
}

//domain 설정
project(":domain") {
    val jar: Jar by tasks
    val bootJar: BootJar by tasks

    bootJar.enabled = false
    jar.enabled = true
}

[domain] build.gradle.kts

AS-IS

plugins {
    kotlin("jvm") version "1.6.10"
    java
}

group = "com.gorany"
version = "0.0.1-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks.getByName<Test>("test") {
    useJUnitPlatform()
}

 

TO-BE

plugins {

}

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

noArg {
    annotation("javax.persistence.Entity") // @Entity가 붙은 클래스에 한해서만 no arg 플러그인을 적용
    annotation("javax.persistence.Embeddable")
    annotation("javax.persistence.MappedSuperclass")
}

dependencies {

}

 

[api] build.gradle.kts

AS-IS

plugins {
    kotlin("jvm") version "1.6.10"
    java
}

group = "com.gorany"
version = "0.0.1-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks.getByName<Test>("test") {
    useJUnitPlatform()
}

 

TO-BE

plugins {

}

dependencies {

}

결과 확인

이제 api 프로젝트에서 domain의 SampleUser를 가지고 코딩할 수 있는지 확인해보자.

 

api 프로젝트에 BootApplication을 만들어주고,

package com.gorany

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ApiApplication

fun main(args: Array<String>) {
    runApplication<ApiApplication>(*args)
}

 

SampleUserRepository를 만들어준다.

package com.gorany.repository

import com.gorany.entity.SampleUser
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.*
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.data.jpa.repository.JpaRepository

@DataJpaTest
class SampleUserRepositoryTest(
    @Autowired val sampleUserRepository: SampleUserRepository
) {

    @Test
    @DisplayName("샘플유저 생성 테스트")
    fun create_sample_user() {

        //given
        val name = "sample"
        val age = 15

        val sampleUser = SampleUser(name = name, age = age)
        //when
        val result = sampleUserRepository.save(sampleUser)

        //then
        assertThat(result.id).isGreaterThan(0)
        assertThat(result.name).isEqualTo(name)
    }
}

테스트 성공. api에서 domain의 SampleUser를 정상적으로 사용함을 확인했다.

 

gradle의 dependencies에서도 api가 domain을 의존하고 있음을 확인하였다.

Local -> Github 

여기서부터는 익숙할 작업일지도 모르니, 가볍게 보아도 좋다.

먼저, root의 경로에서 작업한다.

$ cd ~/IdeaProjects/monorepo

$ git init

//branch name [master] -> [main]
$ git branch -m master main

$ git remote add origin [remote URL]

$ git add .

$ git commit -m "[START] Mono Repo, Multi Module"

$ git push origin main

# Github

https://github.com/rhacnddl/monorepo

 

# Ref

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

https://techblog.woowahan.com/2625/

https://velog.io/@sangwoo0727/Gradle%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88

 

반응형
Comments