본문 바로가기
dev/java

Building a SpringBoot Monorepo with Gradle

by igooo 2024. 11. 6.
728x90

개요

Gradle을 사용하여 프로젝트를 Monorepo로 관리하는 방법에 대하여 설명한다.

 

Monorepo

Mongorepo와 multi-repo에 대하여 장단점이 있지만 어떤 기술이나 그렇지만 프로젝트 상황에 맞게 선택해서 사용하도록 한다.

두 방법에 차이와 장담점에 대해서는 아래 링크에 잘 설명되어 있다.

https://www.thoughtworks.com/insights/blog/agile-engineering-practices/monorepo-vs-multirepo

 

개인적인 생각으로는 프로젝트 인원이 적은 경우 Monorepo가 더 효율적이었고, 깃 브랜치 전략에 따라서도 프로젝트 상황에 맞게 선택하면 좋다.

 

 

프로젝트 구조

Java 프로젝트로 구성할 예정이고 Gradle로 프로젝트를 구성한다.

 

shop - shop-core

         - shop-api

         - shop-batch

 

예시로 쇼핑몰 구성을 위한 shop 프로젝트를 생성하고, 공통 코드를 관리한 shop-core와 shop-code 프로젝트의 의존성을 가지는 shop-api, shop-batch 프로젝트로 프로젝트를 구성한다.

 

시작하기 전 ...

대부분의 Monorepo 예제에서는 서브 프로젝트를 여러 개 생성하고 의존성 관리 및 빌드 구성하는 방법에 대해서만 설명한다. 하지만 실제 프로젝트를 Multi-Project로 만들어 프로젝트를 진행해 봤다면 common(우리 예제에서 shop-core) 프로젝트를 어떻게 구성하냐에 따라서 개발 이후 유지보수에 얼마나 많은 시간이 들어가는지 알 수 있다.

 

common의 중요한 모델이나 로직을 추가하고 모든 프로젝트에서 참조를 한다면 이후에는 common의 저주라고 불릴 만큼 전체 코드는 common에 변경에 대하여 밀접한 관계를 가지게 된다. 예를 들면 common의 변경사항을 모든 프로젝트에 영향을 줄 수 있다.

common 프로젝트는 구성하되 모든 프로젝트에서 사용하는 util 클래스만 포함하고, 모든 프로젝트에서 spring을 사용하더라도 common에서는 spring에 대한 의존성도 가지지 않도록 작게 유지하는 방법이 좋다고 생각한다.

 

추가적으로 Monorepo로 프로젝트를 구성하면서도 Spring Modulith나 Hexagonal Architecture에(https://blog.igooo.org/147) 대해서고 같이 고민하고 구성하도록 하자.

 

Gradle Multi-Project : https://docs.gradle.org/current/userguide/intro_multi_project_builds.html

Spring Modulith : https://spring.io/projects/spring-modulith

 

 

 

Getting Started

 

Monorepo 프로젝트 생성

shop 디렉터리를 생성하고 gradle init 명령을 사용하여 gradle 프로젝트를 구성한다.

$ mkdir shop
$ cd shop
$ gradlew init

Welcome to Gradle 8.10.2!

Here are the highlights of this release:
 - Support for Java 23
 - Faster configuration cache
 - Better configuration cache reports

For more details see https://docs.gradle.org/8.10.2/release-notes.html

Starting a Gradle Daemon, 2 incompatible and 3 stopped Daemons could not be reused, use --status for details

Found existing files in the project directory: 'D:\workspace\shop'.
Directory will be modified and existing files may be overwritten.  Continue? (default: no) [yes, no] yes

Select type of build to generate:
  1: Application
  2: Library
  3: Gradle plugin
  4: Basic (build structure only)
Enter selection (default: Application) [1..4] 4

Project name (default: shop):

Select build script DSL:
  1: Kotlin
  2: Groovy
Enter selection (default: Kotlin) [1..2] 2

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no]


> Task :init
Learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.10.2/samples

BUILD SUCCESSFUL in 19s
1 actionable task: 1 executed

 

 

하위 프로젝트 생성

shop-core, shop-api, shop-batch 3개의 하위 프로젝트를 생성한다.

$ mkdir shop-core shop-api shop-batch

 

settings.gradle 파일에 위에 디렉토리를 추가한다.

rootProject.name = 'shop'

include 'shop-core', 'shop-api', 'shop-batch'

 

gradle 명령을 사용하여 작업 내용을 확인한다.

$ gradlew -q projects

Projects:

------------------------------------------------------------
Root project 'shop'
------------------------------------------------------------

Root project 'shop'
+--- Project ':shop-api'
+--- Project ':shop-batch'
\--- Project ':shop-core'

To see a list of the tasks of a project, run gradlew <project-path>:tasks
For example, try running gradlew :shop-api:tasks

 

 

하위 프로젝트 개발

IDE를 사용하여 프로젝트를 열고 Java 프로젝트처럼 디렉터리를 구성하고, 각 프로젝트에 build.gradle, gradle.properties 파일을 생성한다.

 

shop 프로젝트

shop 프로젝트(root) build.gradle 파일에 아래와 같이 설정한다. subprojects 안에 설정은 모든 하위 프로젝트가 공유한다.

java로 프로젝트를 구성할 예정이라 아래와 같이 java에 대한 설정만 추가한다.

subprojects {
    apply plugin: 'java'

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    repositories {
        mavenCentral()
    }

    dependencies {

    }
}

 

shop-core 프로젝트

shop-core 프로젝트는 라이브러리 역할을 하며 다른 하위 프로젝트에 의존하지 않는다. 그리고 spring 의존성도 가지지 않는 java 프로젝트로만 구성할 예정이고, 예제에서 사용할 Util 클래스와 테스클래스를 추가한다.

 

# build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.5'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

tasks.named('test') {
    useJUnitPlatform()
}

# gradle.properties
group = org.igooo.shop
version = 0.0.1
description = shop-core

 

public class MonorepoUtils {
    public static String name(){
        return "monorepo";
    }
}

class MonorepoUtilsTests {
    @Test
    void name() {
        assertEquals(MonorepoUtils.name(), "monorepo");
    }

}

 

 

shop-api 프로젝트

shop-api 프로젝트는 spring-boot-starter-web을 사용하여 API를 제공하는 프로젝트로 shop-core 프로젝트를 라이브러리로 사용한다. 

# build.gradle
plugins {
    id 'org.springframework.boot' version '3.3.5'
    id 'io.spring.dependency-management' version '1.1.6'
}

dependencies {
    implementation project(":shop-core")

    implementation 'org.springframework.boot:spring-boot-starter-web'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}


# gradle.properties
group = org.igooo.shop
version = 0.0.2
description = shop-api

 

@RestController
class UserApiController {
    @GetMapping("/")
    public Map<String, Object> user() {
        return Map.of("name", MonorepoUtils.name());
    }
}

class UserApiControllerTests {
    @Test
    void user(){
        var controller = new UserApiController();

        assertThat(controller.user()).containsEntry("name", "monorepo");

    }
}

@SpringBootApplication
public class ShopApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopApiApplication.class, args);
    }
}

 

shop-batch 프로젝트

shop-batch 프로젝트는 shop-api와 동일한 구성이지만 spring-boot-starter-batch를 사용한다.

# build.gradle
plugins {
    id 'org.springframework.boot' version '3.3.5'
    id 'io.spring.dependency-management' version '1.1.6'
}

dependencies {
    implementation project(":shop-core")

    implementation 'org.springframework.boot:spring-boot-starter-batch'

    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.batch:spring-batch-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}


# gradle.properties
group = org.igooo.shop
version = 0.0.3
description = shop-batch

 

@SpringBootApplication
public class ShopBatchApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShopBatchApplication.class, args);
    }

    @Bean
    MonorepoUtils monorepoUtils(){
        return new MonorepoUtils();
    }
}


@SpringBootTest
class ShopBatchApplicationTests {
    @Autowired
    private MonorepoUtils monorepoUtils;

    @Test
    void contextLoads() {
        assertThat(monorepoUtils).isNotNull();
    }

}

 

 

Gradle을 사용하여 Monorepo 프로젝트 빌드

$ gradlew clean build
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-11-06T00:24:32.546+09:00  INFO 16776 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-11-06T00:24:32.546+09:00  INFO 16776 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.10.2/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 4s
19 actionable tasks: 19 executed

# 재 빌드하면 Gradle이 캐시를 사용하여 빌드 시간을 빠르게 한다.
$ gradlew build

BUILD SUCCESSFUL in 929ms
16 actionable tasks: 16 up-to-date

 

빌드 완료 후 하위 프로젝트에 gradle.properties에 설정한 버전으로 jar 파일들이 모두 생성되었다.

 

 

Gradle을 shop-api 실행

gradle 명령을 사용하여 shop-api를 실행 후 브라우저를 사용하여 접속하면 Json 결과를 볼 수 있다.

$ gradlew :shop-api:bootRun

> Task :shop-api:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.5)

2024-11-06T00:29:24.453+09:00  INFO 16044 --- [           main] org.igooo.shop.api.ShopApiApplication    : Starting ShopApiApplication using Java 21.0.2 with PID 16044 (D:\workspace\shop\shop-api\build\classes\java\main started by igooo in D:\workspace\shop\shop-api)
2024-11-06T00:29:24.453+09:00  INFO 16044 --- [           main] org.igooo.shop.api.ShopApiApplication    : No active profile set, falling back to 1 default profile: "default"
2024-11-06T00:29:24.859+09:00  INFO 16044 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2024-11-06T00:29:24.859+09:00  INFO 16044 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-11-06T00:29:24.859+09:00  INFO 16044 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.31]
2024-11-06T00:29:24.891+09:00  INFO 16044 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-11-06T00:29:24.891+09:00  INFO 16044 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 420 ms
2024-11-06T00:29:25.050+09:00  INFO 16044 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-11-06T00:29:25.051+09:00  INFO 16044 --- [           main] org.igooo.shop.api.ShopApiApplication    : Started ShopApiApplication in 0.77 seconds (process running for 0.927)
<===========--> 88% EXECUTING [6s]
> :shop-api:bootRun

 

 

'dev > java' 카테고리의 다른 글

Java 23 : Structured Concurrency  (0) 2024.09.28
Java - ReentrantLock  (0) 2024.07.19
Generational ZGC in Java 21  (0) 2024.07.02
Java Virtual Threads 사용 시 synchronized 주의  (0) 2024.06.04
Virtual Threads  (0) 2024.06.02