본문 바로가기
한화시스템 Beyond SW Camp/백엔드

[Spring Boot] 멀티 모듈 프로젝트

by taeh00n 2025. 2. 22.

멀티 모듈(Multi-Module) 프로젝트 : 하나의 큰 프로젝트를 여러 개의 작은 모듈로 나누어 관리하는 구조

각 모듈들은 독립적으로 개발, 빌드가 될 수 있지만 서로 필요한 기능을 공유할 수가 있다.

 

각 모듈은 api 모듈 (비즈니스 로직), common 모듈(공통 유틸리티) 와 같이 하나의 역할만 담당한다.

 

단일 모듈전체 프로젝트를 한번에 빌드해야하고 멀티 모듈개별로 빌드 가능하다.

멀티 모듈은 보통 대규모 프로젝트에 많이 쓰이고 단일 모듈은 소규모 프로젝트에 많이 쓰인다. 그치만 멀티 모듈은 의존성 관리가 다소 복잡할 수 있다는 단점이 있다.

 

재사용성 : 공통 기능(common 모듈)을 다른 모듈에서 쉽게 재사용 가능

빠른 빌드 : 변경된 모듈만 빌드하면 되니 시간 절약

유지보수 용이 : 문제 생기면 해당 모듈만 수정

 


멀티 모듈 프로젝트 생성하기

루트 프로젝트 생성

루트 프로젝트하위 프로젝트의 관리하는 역할만 하고 따로 소스코드는 작성하지 않기 때문에 src 폴더 전체를 삭제해도 된다.

모듈 생성

루트 프로젝트에서 모듈들 생성을 한다.

 

위와 같이 모듈들을 생성하였다. board-api, user-api는 비즈니스 로직, common은 공통 유틸리티, gateway는 요청을 라우팅 해주는 모듈, discovery는 서비스 위치 등록하고 찾아주는 모듈 들을 생성해보았다.

 

Gateway : 클라이언트의 요청을 적절한 서비스로 전달하는 모듈. 인증/인가 등의 작업도 수행한다.

Discovery : 각 서비스의 위치(주소와 포트)를 등록하고 관리하는 역할을 수행하는 모듈. 서비스들을 Eureka 서버에 등록하면 다른 서비스나 Gateway가 Eureka를 통해 동적으로 해당 서비스를 찾을 수 있음.

 

각 모듈들의 공통된 의존성 주입은 루트 프로젝트의 build.gradle에서 설정할 수 있고 각 모듈에만 필요한 의존성이라면 해당 모듈의 build.gradle에서 의존성 주입을 해준다. 

build.gradle과 src 파일만 있으면 해당 하위 모듈의 빌드를 수행할 수 있기 때문에 나머지 파일은 모두 삭제해도 된다.

하위 프로젝트들을 Unlink Gradle Project를 통해서 독립적인 빌드 환경으로 변경해준다.

 

루트 프로젝트에서 settings.gradle 설정

rootProject.name = 'multimodule'

include 'board-api'
include 'user-api'
include 'discovery'
include 'common'
include 'gateway'

루트 프로젝트의 settings.gradle에 하위 프로젝트를 포함시켜준다. 이렇게 되면 include를 통해 하위 모듈들을 다시 루프 프로젝트의 빌드 시스템에 포함되게 한다. 이 과정은 독립적인 Gradle 프로젝트에서 루트 프로젝트의 의존성 및 설정을 다시 상속받게 하는 과정이다.


각 모듈 설정 (build.gradle, application.yml)

server:
  port: 8081

각 하위 모듈들의 application.yml 파일에서 포트 번호를 겹치지 않게 설정을 해줘야 하위 모듈들을 각각 실행해도 오류가 나지 않는다.

 

루트 프로젝트

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.2'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

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

bootJar.enabled = false

repositories {
    mavenCentral()
}
subprojects {
    compileJava {
        sourceCompatibility = 17
        targetCompatibility = 17
    }
    apply plugin: 'java'
    apply plugin: 'java-library'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    repositories {
        mavenCentral()
    }

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

        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
    }

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

}


게이트 웨이

build.gradle


group = 'com.example'
version = '0.0.1-SNAPSHOT'

ext {
    set('springCloudVersion', '2024.0.0')
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

application.yml

server:
  port:
    8080

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka/
    registerWithEureka: false
    fetchRegistry: true

spring:
  cloud:
    gateway:
      routes:
        - id: user-api
          uri: lb://user-api
          predicates:
            - Path=/user/**
        - id: board-api
          uri: lb://board-api
          predicates:
            - Path=/board/**
          filters:
            - JwtFilter

Discovery

build.gradle

group = 'com.example.discovery'
version = '0.0.1-SNAPSHOT'

ext {
    set('springCloudVersion', '2024.0.0')
}

dependencyManagement {
    imports {
       mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
}

application.yml

server:
  port: 8001

spring:
  application:
    name: service-discovery
eureka:
  server:
    eviction-interval-timer-in-ms: 60000  # ???? ?? ?? (???: 60?)
    renewal-percent-threshold: 0.85  # ?? ??? ?? (???: 0.85)
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false

user-api

build.gradle

group = 'com.example.user'
version = '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', '2024.0.0')
}

dependencyManagement {
    imports {
       mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

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

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.mariadb.jdbc:mariadb-java-client'
}

application.yml

server:
  port: 8082

eureka:
  instance:
    lease-renewal-interval-in-seconds: 10
    lease-expiration-duration-in-seconds: 30
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka/
    registerWithEureka: true
    fetchRegistry: true

spring:
  application:
    name: user-api
  datasource:
    url: ${DB_URL}
    driver-class-name: org.mariadb.jdbc.Driver
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    database-platform: org.hibernate.dialect.MariaDBDialect
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.orm.jdbc.bind: trace

board-api

build.gradle


group = 'com.example.board'
version = '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
}
ext {
    set('springCloudVersion', '2024.0.0')
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}


dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation project(path: ':user-api')
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.mariadb.jdbc:mariadb-java-client'
}

application.yml

server:
  port: 8081

eureka:
  instance:
    lease-renewal-interval-in-seconds: 10
    lease-expiration-duration-in-seconds: 30
    prefer-ip-address: true

  client:
    service-url:
      defaultZone: http://localhost:8001/eureka/
    registerWithEureka: true
    fetchRegistry: true

spring:
  application:
    name: board-api
  datasource:
    url: ${DB_URL}
    driver-class-name: org.mariadb.jdbc.Driver
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    database-platform: org.hibernate.dialect.MariaDBDialect
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.orm.jdbc.bind: trace