(Spring) 멀티 모듈 1편
📝 작성 배경
사이트 프로젝트를 진행하면서 스프링 환경에서 멀티모듈 프로젝트를 구성하며, 발생했던 문제들과 해결 방법에 대해 정리해보면 좋을것 같아 이번 주제는 멀티모듈로 선택했다.
글이 조금 길어질수도 있을것 같아 여러편에 나눠 작성하려한다.
1편에서는 멀티 모듈 구성 방법에 대해 중점적으로 설명할 예정이고, 2, 3편에서는 스프링 환경에서 멀티 모듈을 구성했을때 모듈간 의존성 등 멀티 모듈을 구성하면서 발생했던 문제점들에 대해 정리할 예정이다.
먼저 프로젝트 환경을 간단하게 소개하자면 아래와 같다.
🔧 기술 스택
- Spring Boot 3.4.1
- Kotlin 2.1.0
- Gradle 8.12.1
➕ 구성 방법
- 도메인 중심 아키텍처 사용
Convention Plugins
방식 사용Version Catalog
를 활용한 버전 관리
세부적인 내용은 글을 작성하면서 다뤄볼 예정이다.
👓 선 3줄 요약
- 스프링 멀티 모듈 구성 방식으로 레이어드 아키텍처(계층별)와 도메인 중심 아키텍처(도메인별)를 비교하고, MSA 전환을 고려해 도메인 중심 방식을 선택했다.
- Convention Plugin과 Version Catalog를 활용해 여러 모듈 간 중복 코드를 제거하고 일관된 빌드 설정을 관리하는 방법을 소개했다.
- build-logic 방식의 Convention Plugin은 초기 설정에 공수가 들지만, 장기적으로 유지보수성과 확장성이 뛰어나 대규모 프로젝트에 적합하다.
✨ Information
멀티 모듈 아키텍처
멀티 모듈을 구성하는 방식에는 크게 2가지가 있다.
- 레이어드 아키텍처
- 수평적 구조(Horizontal), 계층형 구조, 레이어별 모듈화 등의 용어로 불리고 있다.
- 계층별로 모듈을 분리
- 도메인 중심 아키텍처
- 수직적 구조(Vertical) 등의 용어로 불리고 있다.
- 비즈니스 도메인별로 모듈을 분리
레이어드 아키텍처를 먼저 살펴보자면 살펴보자면, 모듈을 레이어드 아키텍처에 맞게 기능별로 나눈거라 생각하면 쉽다.
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
project-root/
├── api/ # 컨트롤러, API 엔드포인트
│ ├─ common
│ ├─ user
│ ├─ board
│ └─ article
├── core/ # 핵심 도메인 모델, 엔티티
│ ├─ common
│ ├─ user
│ ├─ board
│ └─ article
├── service/ # 비즈니스 로직 계층
│ ├─ common
│ ├─ user
│ ├─ board
│ └─ article
├── repository/ # 데이터 접근 계층
│ ├─ common
│ ├─ user
│ ├─ board
│ └─ article
└── infra/ # 인프라 관련 설정, 외부 서비스 연동
├─ common
├─ user
├─ board
└─ article
간단한 예제로 위 처럼 레이어별로(api, service, core, repository, infra) 모듈을 나누고 해당 모듈안에 각각 도메인 별로 기능을 구현한 모습이라 생각하면 된다. 프로젝트가 소규모 면서, 다양한 도메인에서 공통적으로 사용되는 기능이 많을때 적합하다.
장단점
장점
- 진입 장벽이 낮다.
- 여러 도메인에서 사용되는 공통 기능을 효과적으로 관리 가능하다. 단점
- 하나의 기능 구현에 여러 모듈을 동시에 변경해야 한다.
- 하나의 비즈니스 기능이 여러 모듈에 걸쳐 있어 파악하기 어렵다.
- 계층간 의존성이 복잡해질 수 있다.
다음으로 도메인 중심 아키텍처를 살펴보면 도메인별로 모듈을 나누고 해당 도메인에 필요한 기능을 하나의 모듈에 모아둔거라 생각하면 쉽다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
project-root/
├── user/ # 사용자 관련 모든 기능
│ ├── domain/
│ ├── repository/
│ ├── service/
│ └── api/
├── board/ # 게시판 관련 모든 기능
│ ├── domain/
│ ├── repository/
│ ├── service/
│ └── api/
└── article/ # 게시글 관련 모든 기능
├── domain/
├── repository/
├── service/
└── api/
간단한 예제로 위 처럼 도메인별로(user, board, article) 모듈을 나누고 해당 모듈안에 각각 필요한 기능들을 하나의 모듈에 구현한 모습이라 생각하면 된다. 프로젝트 규모가 크고, 비즈니스 도메인별로 팀이 구성되어 있을때 적합하다.
(MSA 를 고려하고 하는 경우에도 좋다.)
장단점
장점
- 도메인별로 하나의 모듈에 모여있다 보니 응집도가 높다.
- 독립적인 개발 및 배포가 가능하다.
- MSA 로 전환하기 편리하다. 단점
- 여러 도메인에서 유사한 기능 구현 시 코드 중복이 발생할 수 있다.
- 초기 도메인 설계가 잘못되는 경우 추후 변경하기 매우 까다롭다.
필자가 진행하는 사이드 프로젝트에서는 추후 MSA 의 전환을 고려하고 있어 두번째 방식인 도메인 중심 아키텍처를 사용했다. 다만, 초기 설계가 조금 어려웠고 작업 공수가 많이 들어간다는 단점이 있다.
현재 상황 또는 도전하고 싶은 내용이 있을 때 등 현재 내가 원하는 상황에서 적합한 방식을 선택하여 사용하면 될것 같다.
Convention Plugins
Gradle
의 Convention Plugin
은 프로젝트 간에 공통 구성과 기능을 공유하기 위한 매커니즘 중 하나이다. 특히 멀티 프로젝트 빌드나 여러 팀이 공유하는 일관된 구성이 필요할 때 유용하다.
멀티 모듈 환경에서 build.gradle
작성에서 공통된 부분을 줄일 수 있고, 일관성 있게 관리하기 매우 좋은 방식이다. 여러 모듈에서 build.gradle
을 작성하다보면 중복되는 부분이 있기 마련이고, 일관성을 유지하기 어려운데 이런 문제점을 해결해줄 수 있는 방식이다.
Convention Plugin
을 구성하는 방식에도 여러 방식이 있는걸로 알고 있는데 나는 그중 build-logic
을 활용한 구성 방식을 사용했다.
또 다른 방식으로는 buildSrc
를 활용하는 방식인데 둘 다 설명하기에는 글이 너무 길어 질것 같아 Gradle 공식 문서 링크를 걸어두겠다. 혹시 해당 내용을 좀 더 알고 싶다면 검색을 통해 알아보는것도 좋을것 같다.
두 방식 모두 Convention Plugin
으로 불리고 있는데, 간단하게 차이점만 알아보고 넘어가도록 하자.
비교 항목 | buildSrc | build-logic (Composite Build) |
---|---|---|
구성 방식 | 프로젝트 루트 내 buildSrc 폴더 자동 감지 | includeBuild("<경로>") 로 명시적 포함 필요 |
빌드 로직 적용 범위 | 해당 프로젝트 내 모든 서브 프로젝트 자동 적용 | 명시적으로 포함한 프로젝트에서만 사용 가능 |
공유 가능성 | 단일 프로젝트 내 한정 | 여러 프로젝트 간 공통 빌드 로직 공유 가능 |
빌드 성능 | 변경 시 전체 빌드 무효화 및 재컴파일 | 변경된 부분만 증분 빌드, 빌드 성능 개선 |
빌드 로직 격리 | 프로젝트 내부에 포함되어 격리 수준 낮음 | 별도 빌드로 격리되어 관리 및 성능 측면에서 유리 |
사용 편의성 | 자동 감지로 간편하지만 대규모 프로젝트에서 비효율적일 수 있음 | 수동 관리 필요하지만 대규모 프로젝트에 적합 |
Gradle 7.0
이후 buildSrc
보다 build-logic
을 활용하는걸 추천하고 있어 멀티 모듈 구성 시 해당 방식을 주로 사용하고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
project-root/
├── build-logic/ # build-logic 모듈
│ ├── convention/
│ │ ├── src/ # convention plugin 코드
│ │ └── build.gradle.kts
│ ├── gradle.properties
│ └── settings.gradle.kts
├── gradle/
│ ├── wrapper/
│ └── libs.versions.toml
├── build.gradle.kts
└── settings.gradle.kts
수동 관리를 위해 위와 같이 build-logic
모듈을 선언하고, 해당 모듈 내부에 여러 모듈에서 사용할 Convention Plugin
을 만들고 각각의 모듈에서 해당 Plugin
을 선언하여 사용하면 된다.
추가적으로 라이브러리 버전을 공통적으로 관리하기 위해 libs.versions.toml
파일을 사용하여 관리했다.(Version Catalog)
해당 방식을 사용하지 않고 Convention Plugin
내부에 버전을 명시해서 사용할 수 도 있는데 이 경우 버전 관리에 불편함을 느껴 도입하게 되었다.(여러 plugin 에 버전 명시가 나눠져 있는 경우 어디서 버전을 선언하고 있는지 확인하는 부분에서의 불편함이 컸다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[versions]
kotlin = "2.1.0"
spring-boot = "3.4.1"
# ...
[libraries]
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "spring-boot" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }
spring-boot-gradle-plugin = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin", version.ref = "kotlin" }
# ...
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
# ...
Version Catalog
의 toml
파일의 일부분이다.
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
32
33
34
35
36
37
38
39
import com.crispinlab.libs
import com.crispinlab.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import com.crispinlab.configureKotlinJvm
class KopringWebLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
plugins(
"org.springframework.boot",
"io.spring.dependency-management",
"org.jetbrains.kotlin.plugin.spring",
"spring.test.library",
"kotlin.serialization"
)
dependencies {
add("implementation", libs.findLibrary("spring.boot.starter.web").get())
add("implementation", libs.findLibrary("spring.boot.starter.validation").get())
}
}
}
}
class JvmLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
plugins(
"java-library",
"org.jetbrains.kotlin.jvm",
"kotest.library"
)
configureKotlinJvm()
}
}
}
Convention Plugin
중 kotlin + springboot + springboot starter web
을 사용할 수 있도록 정의한 Plugin
코드이다.
여러 가지 설정들이 추가적으로 필요하긴 한데, 이부분은 2편에서 자세하게 다룰 예정이다.
위 처럼 버전관리 및 플러그인을 선언하게 되면 각각의 모듈의 build.gradle.kts
파일에 해당 부분만 추가하게 되면 kotlin + springboot + springboot starter web
을 사용할 수 있도록 설정이 된다.
1
2
3
4
5
plugins {
alias(libs.plugins.jvm.library)
alias(libs.plugins.kotlin.spring.web)
// ...
}
기존에는 멀티 모듈 구성 시 모듈 마다 build.gadle.kts
파일에 라이브러리 의존성 추가, 모듈간 의존성 추가, 버전 추가 등등 코드의 양이 매우 많아지고 그로인해 관리하기 어려운점이 있었는데 해당 방식을 도입하고 나서는 그런 어려움이 많이 줄어들게 되었다.
다만, 해당 방식 도입 시 초기 설정에 어려움이 많아 도입을 생각하고 있다면 현재 프로젝트에 바로 적용하지 말고 새로운 프로젝트 생성 후 작게나마 POC(Proof of Concept) 를 진행해 보고 도입하는걸 권장한다.
Convention Plugin
을 어떤 부분으로 나눌것이며, 향후 라이브러리 추가 시 어떻게 추가해야하는지 등 고려해야할 부분이 조금있어 처음 사용한다고 했을때는 막히는 부분이 조금 있을거라 예상된다.
필자는 우선 사이드 프로젝트에서 사용해보고, 사용해본 내용을 바탕으로 팀 내부적으로 협의하여 도입을 고려할 예정이다.
💡 마무리
간단하게 Spring + Kotlin + Gradle
환경에서 멀티 모듈을 구성했던 방법에 대해 작성해봤다.
해당 방식이 멀티 모듈을 구성하는데 있어 정답은 아닐것이다. 다만, 필자가 멀티 모듈을 도입 했을때 여러 고민을 했었고, 고민 끝에 해당 방식으로 멀티 모듈을 구성하여 사이드 프로젝트를 진행하고 있으며 멀티 모듈 구성 방식에는 이런 방법도 있다는걸 보여주고 싶었다. 1편은 정말 간단하게 어떤식으로 멀티 모듈을 구성 했는지에 대한 내용만 적었다. 세부적인 내용은 2편, 3편(더 필요하다면 4, 5편 등등) 에서 다뤄볼 예정이다.