7 분 소요


프로젝트 시작전

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

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

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

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

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

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

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

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

프로젝트 구성하기

  • 프로젝트 구조

    - 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)

프로젝트 세팅하기

  1. SpringBoot-Multimodules를 만든다.

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

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

         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들은 각각의 모듈에서 관리한다.

  2. gradle project를 생성한다.

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

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

    • docker-compose.yml을 root에 놓고 docker-compose up -d 명령어로 실행한다.

      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 schema springtest;
      
    • docker-compose up -d 명령어로 실행한다.
  • 여기까지 하면, 아래의 그림처럼 폴더가 나온다

Gradle 작업하기

  • 여러 프로젝트가 하나로 모였기 때문에 우리가 Build하는 시스템인 Gradle에게 알려주어야한다.

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

  • root project의 settings.gradle.kts 에 모듈을 알려준다

    // settings.gradle.kts
    rootProject.name = "SpringBoot-Multimodules"
    include("module-stream")
    include("module-api")
    include("module-core")
    
  • root프로젝트에 작업이 완료되고나면, build.gradle.kts 를 수정해준다.
  • 나의경우는 아래와 같은데, 소스를 참고해서 각 프로젝트별로 어떻게 다른지 확인해서 적용하면 될 것 같다.

    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 작업하기

  • module-core의 build.gradle.kts 를 아래와 같이 수정한다

    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 작업하기

  • module-api에 build.gradle.kts 를 아래와 같이 수정한다

    plugins{
    
    }
    
    dependencies{
    
    }
    
    

module-stream에 Gradle 작업하기

  • module-stream에 build.gradle.kts 를 아래와 같이 수정한다

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

세팅을 마치며

  • 세팅 하는데 꽤 오래걸린 것 같다. 이것저것 삽질도 많이 했다.
  • 중요한 포인트는, 패키지들을 만들고 모듈을 만들 때 “패키지 구조”를 같게 해 주어야 한다.
    • 예를 들어, com.wool로 패키지를 만들었다면 다른 모듈에서도 동일하게 com.wool로 생성 해 주어야 한다.

참고

Before Starting the Project

While working on project sprints 1 and 2 using SpringBoot, I ended up creating some really basic but fundamental features.

This ongoing project could consist 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 entities were being duplicated.

I thought about applying multi-module to share entities commonly and use other unique features in each server.

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

While reading, you might wonder “why is it like that?” - I understand because I had a lot of concerns while writing this too.

Rather than saying “each part does its appropriate role,” I wanted to show how to configure modules, and hopefully it’ll be useful to think about and apply when developing later.

Configuring the Project

  • Project Structure

    - SpringBoot-Multimodules
      - module-api
      - module-core
      - module-stream
    
    • module-api
      • API Server
      • Creates and retrieves current users via API calls
      • Retrieves orders received via 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 DB (Consumer)

Setting Up the Project

  1. Create SpringBoot-Multimodules.

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

    • I created it with SpringBoot and received 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")
         }
      
    • Since SpringBoot-Multimodules is a project that manages individual modules, dependencies are managed in each module.

  2. Create gradle projects.

    • Each module is created as a gradle project.
    • In my case, I created module-api, module-core, and module-stream.
  3. 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 in SpringBoot-Multimodules.
  4. Install the database locally. (In my case, I used docker-compose to run postgresql)

    • Place docker-compose.yml in the root and run 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
      
    • Create a local-db folder and write init_schema.sql.
      create schema springtest;
      
    • Run with the docker-compose up -d command.
  • After completing this, the folder structure looks like the image below

Working with Gradle

  • Since multiple projects are combined into one, we need to inform Gradle, our build system.

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

  • Inform the root project’s settings.gradle.kts about the modules

    // settings.gradle.kts
    rootProject.name = "SpringBoot-Multimodules"
    include("module-stream")
    include("module-api")
    include("module-core")
    
  • After completing work on the root project, modify build.gradle.kts.
  • My configuration is as below - refer to the source and check how it differs for each project to 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-api and consumer depend on module-core
    project(":module-api") {
        dependencies {
            implementation(project(":module-core"))
        }
    }
    
    project(":module-stream") {
        dependencies {
            implementation(project(":module-core"))
        }
    }
    
    // core configuration
    project(":module-core") {
        val jar: Jar by tasks
        val bootJar: BootJar by tasks
    
        bootJar.enabled = false
        jar.enabled = true
    
    }
    

Working with Gradle on module-core

  • 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") // Apply no arg plugin only to classes with @Entity
        annotation("javax.persistence.Embeddable")
        annotation("javax.persistence.MappedSuperclass")
    }
    
    dependencies{
    
    }
    
    

Working with Gradle on module-api

  • Modify module-api’s build.gradle.kts as follows

    plugins{
    
    }
    
    dependencies{
    
    }
    
    

Working with Gradle on module-stream

  • 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

  • The setup took quite a while. I did a lot of trial and error.
  • The important point is that when creating packages and modules, the “package structure” must be the same.
    • For example, if you create a package as com.wool, other modules must also be created as com.wool.

Reference

댓글남기기