본문으로 건너뛰기

스프링부트 멀티모듈 구성하기(1) - 프로젝트 구조만들기

프로젝트 시작전

Before Starting the Project

스프링부트를 사용해 프로젝트 sprint 1,2를 진행하면서 정말 기초적이지만 기본기능들을 만들게 되었다

진행하고 있는 이 프로젝트는, 한 개의 API서버, 한 개 이상의 Consumer Application, 차후에 생길 가능성이있는 배치서버 등으로 구성 될 수 있었다.

빠르게 진행되는 프로젝트라서, 백엔드 API 서버와 Stream 어플리케이션 서버를 각각 두었다.

기능들이 잘 동작하고, 스프린트 보고도 잘 끝났지만 소스코드를 놓고 보니 엔티티가 중첩되는 현상을 보았다.

멀티모듈을 적용시켜서 엔티티들을 공통적으로 사용하고, 그외의 고유한 기능들을 각각의 서버에서 사용하자는 생각이 들었다.

한번에 모든 세팅을 보기보다, 하나씩 사용해보고 기능을 확인하면서 전체적인 구조로 다가가려고한다.

보다보면 "왜저러지?" 라는게 있을 수 있는데... 글을 쓰면서도 고민이 많이되었기때문에 이해한다.

"각각 맞는 역할을 한다" 라기보다 모듈을 어떻게 설정했는지 보여주고싶었고, 차후에 개발을 할 때 고민해서 적용 해 보면 좋지 않을까? 한다.

While working on sprint 1 and 2 of my project using Spring Boot, I built some basic but fundamental features.

This project could be composed of one API server, one or more Consumer Applications, and potentially a batch server in the future.

Since the project was moving quickly, I set up the backend API server and Stream application server separately.

The features worked well and the sprint report went smoothly, but when I looked at the source code, I noticed that entities were being duplicated.

I thought about applying multi-module architecture to use entities commonly, while keeping unique features in their respective servers.

Rather than looking at all the settings at once, I wanted to try things one by one and understand the features while approaching the overall structure.

You might wonder "why is it like that?" while reading... I understand because I had many concerns while writing this too.

Rather than saying "each one plays its appropriate role," I wanted to show how to configure modules, and I think it would be good to consider and apply this when developing in the future.

프로젝트 구성하기

Project Configuration

  • 프로젝트 구조
  • Project Structure
- SpringBoot-Multimodules
- module-api
- module-core
- module-stream
  • module-api
    • API 서버
    • API 호출로 현재 사용자를 생성하고 조회한다
    • API 호출로 들어온 order들을 Admin page에 제공하기위해 조회한다
  • module-core
    • API 서버에서 조회하기위한 Entity를 정의한다
    • Stream 서버에서 사용하기위한 Entity를 정의한다
  • module-stream
    • Stream 서버
    • Kafka를 사용해, API요청들을 order topic에 발송한다(Producer)
    • Stream에 발송된 order들을 받아서, DB에 저장한다(Consumer)
  • module-api
    • API Server
    • Creates and retrieves current users through API calls
    • Retrieves orders from API calls to provide to the Admin page
  • module-core
    • Defines Entities for querying in the API server
    • Defines Entities for use in the Stream server
  • module-stream
    • Stream Server
    • Uses Kafka to send API requests to the order topic (Producer)
    • Receives orders sent to the Stream and saves them to the DB (Consumer)

프로젝트 세팅하기

Project Setup

  1. SpringBoot-Multimodules를 만든다.

    • SpringBoot-Multimodules는 Spring application으로 생성해도 되고, gradle project로 생성해도 된다.

    • 나는 SpringBoot로 생성했고, 기본적인 dependency들을 받았다.

  1. Create SpringBoot-Multimodules.

    • SpringBoot-Multimodules can be created as a Spring application or as a gradle project.

    • I created it with SpringBoot and added the basic dependencies.

   dependencies {
// springboot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
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")

// DB
implementation("org.postgresql:postgresql:42.3.3")

// test
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
  • SpringBoot-Multimodules는 각각의 모듈들을 관리하는 프로젝트이기 때문에, dependency들은 각각의 모듈에서 관리한다.
  1. gradle project를 생성한다.

    • 각각의 모듈들은 gradle project로 생성한다.
    • 나의경우, module-api, module-core, module-stream 을 생성했다.
  2. 폴더를 정리 해 준다.

    • SpringBoot-Multimodules 가 root 프로젝트 이지만, 여기서 하는일은 주로 dependency 관리이기 때문에 src 하위 폴더를 모두 지워준다
    • SpringBoot-Multimodules 내에는 module-api, module-core, module-stream 과 gradle 폴더만 남는다.
  3. Database를 로컬에 설치 해 준다. (나의경우 docker-compose로 postgresql을 올렸다)

    • docker-compose.yml을 root에 놓고 docker-compose up -d 명령어로 실행한다.
  • Since SpringBoot-Multimodules is a project that manages each module, dependencies are managed in each module.
  1. Create gradle projects.

    • Each module is created as a gradle project.
    • In my case, I created module-api, module-core, module-stream.
  2. Clean up the folders.

    • Although SpringBoot-Multimodules is the root project, since its main job is dependency management, delete all subfolders under src
    • Only module-api, module-core, module-stream and the gradle folder remain inside SpringBoot-Multimodules.
  3. Install the database locally. (In my case, I used docker-compose to run postgresql)

    • Place docker-compose.yml in the root and run it with the docker-compose up -d command.
version: "3.9"

services:
postgres:
image: postgres:14-alpine
container_name: multimodule-postgres
ports:
- "9876:5432"
volumes:
- .postgresql/:/var/lib/postgresql/data
- ./local-db/init_schema.sql:/docker-entrypoint-initdb.d/1-schema.sql

environment:
- POSTGRES_PASSWORD=password1234
- POSTGRES_USER=wool
- POSTGRES_DB=wooldb
  • local-db 폴더를 만들고, init_schema.sql을 작성한다.
  • Create a local-db folder and write init_schema.sql.
create schema springtest;
  • docker-compose up -d 명령어로 실행한다.

  • 여기까지 하면, 아래의 그림처럼 폴더가 나온다

  • Run it with the docker-compose up -d command.

  • After doing this, the folder structure will look like the image below

Gradle 작업하기

Working with Gradle

  • 여러 프로젝트가 하나로 모였기 때문에 우리가 Build하는 시스템인 Gradle에게 알려주어야한다.
  • Since multiple projects are combined into one, we need to inform Gradle, our build system.

Root Project (SpringBoot-Multimodule)에 Gradle 작업하기

Working with Gradle in the Root Project (SpringBoot-Multimodule)

  • root project의 settings.gradle.kts 에 모듈을 알려준다
  • Inform the modules in the root project's settings.gradle.kts
// settings.gradle.kts
rootProject.name = "SpringBoot-Multimodules"
include("module-stream")
include("module-api")
include("module-core")
  • root프로젝트에 작업이 완료되고나면, build.gradle.kts 를 수정해준다.
  • 나의경우는 아래와 같은데, 소스를 참고해서 각 프로젝트별로 어떻게 다른지 확인해서 적용하면 될 것 같다.
  • After completing the work on the root project, modify build.gradle.kts.
  • In my case it's as follows, but you can refer to the source and check how each project differs and apply accordingly.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.tasks.bundling.BootJar

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

java.sourceCompatibility = JavaVersion.VERSION_17

allprojects {
group = "com.example"
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 {
// springboot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
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")

// DB
implementation("org.postgresql:postgresql:42.3.3")

// test
testImplementation("org.springframework.boot:spring-boot-starter-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 = "17"
}
}

tasks.withType<Test> {
useJUnitPlatform()
}

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

// module core 에 module api, consumer이 의존
project(":module-api") {
dependencies {
implementation(project(":module-core"))
}
}

project(":module-stream") {
dependencies {
implementation(project(":module-core"))
}
}

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

bootJar.enabled = false
jar.enabled = true

}

module-core에 Gradle 작업하기

Working with Gradle in module-core

  • module-core의 build.gradle.kts 를 아래와 같이 수정한다
  • Modify module-core's build.gradle.kts as follows
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{

}

module-api에 Gradle 작업하기

Working with Gradle in module-api

  • module-api에 build.gradle.kts 를 아래와 같이 수정한다
  • Modify module-api's build.gradle.kts as follows
plugins{

}

dependencies{

}

module-stream에 Gradle 작업하기

Working with Gradle in module-stream

  • module-stream에 build.gradle.kts 를 아래와 같이 수정한다
  • Modify module-stream's build.gradle.kts as follows
plugins{

}

dependencies{
implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}

세팅을 마치며

Wrapping Up the Setup

  • 세팅 하는데 꽤 오래걸린 것 같다. 이것저것 삽질도 많이 했다.
  • 중요한 포인트는, 패키지들을 만들고 모듈을 만들 때 "패키지 구조"를 같게 해 주어야 한다.
    • 예를 들어, com.wool로 패키지를 만들었다면 다른 모듈에서도 동일하게 com.wool로 생성 해 주어야 한다.
  • The setup took quite a while. I did a lot of trial and error.
  • The important point is that when creating packages and modules, you need to keep the "package structure" the same.
    • For example, if you created a package as com.wool, you should create it the same way as com.wool in other modules.

참고

References